Quarantined high-risk skills (browser session capture, WebSocket interception) for Slack and Microsoft Teams. Install only after reviewing the security implications.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
// slack-ext-grid.js — Pure logic for slack-ext Enterprise Grid admin commands.
//
// This module contains NO `fs`, `path`, or `sliccy:*` imports so it runs in
// the SLICC tst test realm as a plain CommonJS module. All functions are pure
// (no side effects, no I/O) and are consumed by slack-ext.jsh at runtime.
//
// ── Wire facts (verified live 2026-09-22) ─────────────────────────────────────
//
// ENTERPRISE USER METHODS — all in `enterprise.users.admin.*`:
// enterprise.users.admin.setRestricted → make MULTI-CHANNEL GUEST
// verified: ok:true observed from UI; bogus user → user_not_found
// enterprise.users.admin.setRegular → make FULL MEMBER
// verified: ok:true observed from UI; bogus user → user_not_found
// enterprise.users.admin.setStatus (status=delete) → DEACTIVATE
// TRAP: status=delete means deactivate (reversible), NOT permanent delete.
// verified: ok:true observed from UI; bogus user → invalid_user (different
// from user_not_found — the field is named `user` but Slack validates it
// differently for this endpoint). Observed _x_reason=deactivateMembers.
// users.admin.profileDeidentify → GDPR FORGET (IRREVERSIBLE)
// Real name becomes "Deactivated User", handle becomes deactivateduser<N>,
// guest flags cleared. CANNOT BE UNDONE. Observed _x_reason=forget-user.
// This method is in the WORKSPACE-LEVEL `users.admin.*` namespace despite
// being an org-wide operation.
// enterprise.users.admin.setUltraRestricted → SINGLE-CHANNEL GUEST
// ** UNVERIFIED: method exists (bogus user → user_not_found, confirming
// the endpoint is real) but ok:true was NEVER OBSERVED from the UI.
// Treat as best-effort; marked UNVERIFIED in SKILL.md and in the code. **
//
// TOKEN: enterprise methods use the ORG-LEVEL xoxc token, found in
// localStorage['localConfig_v2'].teams['E06V3987PMY'].token
// (vs workspace-level commands that use teams['T0385CHDU9E'].token).
// Both are xoxc tokens — same auth mechanism, different org/ws scopes.
//
// AUDIT ATTRIBUTION — IMPORTANT NUANCE:
// Session-token (xoxc) calls are INDISTINGUISHABLE FROM THE HUMAN'S OWN
// DIRECT ACTIONS in Slack's channel event history. A concrete case:
// #aem-fedex (C0C2CUUDWLE) was archived by Zapier at 2026-09-17T00:17:04Z;
// the channel event log records Lars Trieloff as the actor, not Zapier,
// because Zapier ran on his user OAuth token. These commands do the same.
// The Enterprise Audit Logs API (auditlogs:read scope) WOULD record the
// acting app and distinguish automation from a human click — but
// admin.audit.* methods return unknown_method (six variants probed, all
// unknown). There is currently no working API call that distinguishes an
// xoxc-based script from a human in the channel event log.
//
// CHANNEL COMMANDS — `admin.conversations.*`:
// admin.conversations.convertToPublic takes channel_id (verified real)
// admin.conversations.convertToPrivate takes channel_id (verified real)
// PRIVATE CHANNEL INVISIBILITY: after converting to private, the channel
// becomes invisible to non-members. conversations.info returns channel_not_found,
// conversations.genericInfo returns ok:true with an EMPTY array, and the edge
// cache returns the id under failed_ids. This is not an error — handle it by
// reporting "channel is now private and not visible to this caller".
//
// CHANNEL SEARCH — `admin.conversations.search`:
// MEASURED DEFECT: the `channel_ids` parameter is SILENTLY IGNORED.
// Passing channel_ids=C0634KMGW2G (bare string) AND channel_ids=["C0634KMGW2G"]
// (JSON array) both returned ok:true with the UNFILTERED full list. NEVER use
// channel_ids as a filter — it silently answers about the wrong channel.
// Instead: enumerate (with cursor) and filter locally. This code enforces it.
//
// SLACK CONNECT APPROVALS — `conversations.sharedApprovals.list`:
// Returns pending (and other) Slack Connect invite approvals.
// Verified: 977 total approvals in the org at time of writing.
// Paginates via response_metadata.next_cursor.
//
// APP GOVERNANCE:
// admin.apps.approve / admin.apps.restrict:
// Either app_id + enterprise_id, OR request_id for a pending install request.
// SINGLE-USE request_id: approving then restricting the same request_id returns
// request_already_resolved. To reverse a resolved request, use app_id + enterprise_id.
// admin.apps.clearResolution — app_id + enterprise_id
// admin.apps.permissions.set — app_id + permission_type (no_one|everyone|named_entities)
// admin.apps.approved.list / admin.apps.restricted.list — enterprise_id + limit
'use strict';
// ── Parameter builders ─────────────────────────────────────────────────────────
//
// All return plain objects — the caller turns them into FormData / URLSearchParams.
// These are the ONLY call sites that know the exact wire parameter names.
// enterprise.users.admin.setRestricted
// Makes the user a multi-channel guest at the org level.
function buildEgSetRestrictedParams(userId) {
return { user: userId };
}
// enterprise.users.admin.setRegular
// Promotes the user to a full member at the org level.
function buildEgSetRegularParams(userId) {
return { user: userId };
}
// enterprise.users.admin.setStatus
// status='delete' = DEACTIVATE (reversible). 'delete' here does NOT mean delete.
// The reactivation status value is NOT KNOWN — do not guess it.
function buildEgSetStatusParams(userId, status) {
return { user: userId, status: status };
}
// users.admin.profileDeidentify
// IRREVERSIBLE GDPR forget. Real name → "Deactivated User", handle →
// deactivateduser<N>, guest flags cleared. CANNOT BE UNDONE.
function buildDeidentifyParams(userId) {
return { user: userId };
}
// enterprise.users.admin.setUltraRestricted
// ** UNVERIFIED: endpoint is real (confirmed via probe — bogus user → user_not_found)
// but ok:true was NEVER OBSERVED from the live admin UI session. Do not call
// without confirming the wire format produces the expected result. **
function buildEgSetUltraRestrictedParams(userId) {
return { user: userId };
}
// admin.conversations.convertToPublic / convertToPrivate
function buildConvertChannelParams(channelId) {
return { channel_id: channelId };
}
// admin.conversations.search
//
// Observed required/significant parameters (2026-09-22):
// search_channel_types — changes the result set materially:
// 'all' — all channels including archived and private
// 'exclude_archived' — non-archived channels only (recommended default)
// 'private' — private channels only
// 'private_exclude' — similar to 'all' in observed behaviour
// 'archived' — archived channels only
// Note: Lars Trieloff observed omitting this returns invalid_arguments
// in the UI path; live probe 2026-09-22 returned ok:true without it,
// suggesting the API defaults internally. Include it explicitly anyway
// because 'all' vs 'exclude_archived' yields 2072 vs 1515 results —
// the difference is significant and silent.
// sort — valid observed values: 'name', 'member_count', 'created'
// 'last_activity_ts' and 'num_members' return invalid_sort
// sort_dir — 'asc' | 'desc'
// query — may be empty string
// cursor — may be empty string
//
// MEASURED DEFECT: channel_ids is SILENTLY IGNORED. Never pass it.
// Filter results locally; see filterChannels() below.
const VALID_SEARCH_CHANNEL_TYPES = new Set(
['all', 'exclude_archived', 'private', 'private_exclude', 'archived']
);
const VALID_CHANNEL_SORT_FIELDS = new Set(['name', 'member_count', 'created']);
function buildChannelSearchParams(query, limit, cursor, searchChannelTypes, sort, sortDir) {
const p = {
// Empty string is accepted; must be present.
query: query || '',
limit: String(limit || 50),
// Observed in UI; omitting silently defaults to something — include explicitly.
search_channel_types: searchChannelTypes || 'exclude_archived',
sort: sort || 'name',
sort_dir: sortDir || 'asc',
// Empty-string cursor is accepted and must be present once pagination starts.
cursor: cursor || '',
};
return p;
}
// conversations.sharedApprovals.list
function buildApprovalsListParams(limit, query, cursor) {
const p = { limit: String(limit || 25), sort: 'date_expire', sort_dir: 'desc' };
if (query) p.query = query;
if (cursor) p.cursor = cursor;
return p;
}
// admin.apps.approve / admin.apps.restrict
// Accepts either a pending-install request_id (I…) or app_id + enterprise_id.
// SINGLE-USE request_id: once resolved, further attempts with the same request_id
// return request_already_resolved. Use app_id + enterprise_id to reverse.
function buildAppApproveRestrictParams(opts) {
if (opts && opts.requestId) {
return { request_id: opts.requestId };
}
return { app_id: opts.appId, enterprise_id: opts.enterpriseId };
}
// admin.apps.clearResolution
function buildAppClearResolutionParams(appId, enterpriseId) {
return { app_id: appId, enterprise_id: enterpriseId };
}
// admin.apps.permissions.set
function buildAppPermissionsParams(appId, permissionType) {
return { app_id: appId, permission_type: permissionType };
}
// admin.apps.approved.list / admin.apps.restricted.list
function buildAppListParams(enterpriseId, limit, cursor) {
const p = { enterprise_id: enterpriseId, limit: String(limit || 50) };
if (cursor) p.cursor = cursor;
return p;
}
// ── ID classifiers ─────────────────────────────────────────────────────────────
// Determine whether input looks like a Slack install-request ID (Ixxxxxxxx)
// or an app ID (Axxxxxxxx). Returns { requestId } or { appId } or null.
function resolveAppOrRequestId(input) {
if (!input || typeof input !== 'string') return null;
if (/^I[A-Z0-9]{6,}$/i.test(input)) return { requestId: input };
if (/^A[A-Z0-9]{6,}$/i.test(input)) return { appId: input };
return null;
}
// Validate a permission_type value for admin.apps.permissions.set.
// Returns true if the value is one of the observed valid options.
const VALID_PERMISSION_TYPES = new Set(['no_one', 'everyone', 'named_entities']);
function isValidPermissionType(t) {
return VALID_PERMISSION_TYPES.has(t);
}
// ── Channel search helpers ─────────────────────────────────────────────────────
// Filter a list of channel objects by name substring or exact ID match.
// IMPORTANT: do NOT pass channel IDs to admin.conversations.search (silent defect).
// Use this function to filter the locally-collected results instead.
function filterChannels(channels, query) {
if (!query) return channels.slice();
const lq = query.toLowerCase();
return channels.filter(function (c) {
return (
(c.id && c.id.toLowerCase() === lq) ||
(c.name && c.name.toLowerCase().includes(lq))
);
});
}
// Summarize a channel object from admin.conversations.search into a concise form.
function summarizeChannel(c) {
return {
id: c.id,
name: c.name,
is_private: c.is_private,
is_archived: c.is_archived,
member_count: c.member_count,
external_user_count: c.external_user_count,
purpose: c.purpose,
created: c.created,
creator_id: c.creator_id,
last_activity_ts: c.last_activity_ts,
};
}
// ── Approval helpers ──────────────────────────────────────────────────────────
// Summarize a sharedApprovals entry into display form.
function summarizeApproval(a) {
return {
id: a.id,
partner_org: a.connecting_team ? a.connecting_team.name : '',
channel: a.channel ? a.channel.name : '',
status: a.status,
expires: a.date_expire,
};
}
// ── User type classification ───────────────────────────────────────────────────
//
// Mirrors userTypeLabel() in slack-ext.jsh. Reproduced here so tests can exercise
// the flag interpretation without importing the main script (which has sliccy:* deps).
function classifyUser(user) {
if (!user) return 'unknown';
if (user.deleted) return 'deactivated';
if (user.is_bot) return 'bot';
if (user.is_ultra_restricted) return 'single-channel guest';
if (user.is_restricted) return 'multi-channel guest';
return 'regular';
}
// ── Pagination helper (pure, injectable fetch) ────────────────────────────────
//
// Collects all pages from a cursor-paginaged Slack endpoint.
// fetchPage: async (cursor) => { ok, items, next_cursor, error }
// itemsKey: the response field that holds the array (e.g. 'conversations', 'approvals')
// maxItems: hard cap (0 = unlimited)
//
// Returns { items, total_fetched, pages, error? }
async function collectPages(fetchPage, itemsKey, maxItems) {
const all = [];
let cursor = '';
let pages = 0;
const cap = maxItems || 0;
do {
const r = await fetchPage(cursor);
if (!r.ok) {
return { items: all, total_fetched: all.length, pages: pages, error: r.error };
}
pages += 1;
const chunk = r[itemsKey] || [];
for (const item of chunk) {
all.push(item);
if (cap > 0 && all.length >= cap) break;
}
// next_cursor may be in response_metadata or directly on the body
const meta = r.response_metadata || {};
cursor = meta.next_cursor || r.next_cursor || '';
if (cap > 0 && all.length >= cap) break;
} while (cursor);
return { items: all, total_fetched: all.length, pages: pages };
}
// ── Exports ────────────────────────────────────────────────────────────────────
module.exports = {
// Parameter builders
buildEgSetRestrictedParams,
buildEgSetRegularParams,
buildEgSetStatusParams,
buildDeidentifyParams,
buildEgSetUltraRestrictedParams, // UNVERIFIED — see wire facts above
buildConvertChannelParams,
buildChannelSearchParams,
buildApprovalsListParams,
buildAppApproveRestrictParams,
buildAppClearResolutionParams,
buildAppPermissionsParams,
buildAppListParams,
// Classifiers and helpers
resolveAppOrRequestId,
isValidPermissionType,
VALID_PERMISSION_TYPES,
VALID_SEARCH_CHANNEL_TYPES,
VALID_CHANNEL_SORT_FIELDS,
filterChannels,
summarizeChannel,
summarizeApproval,
classifyUser,
collectPages,
};