CtrlK
BlogDocsLog inGet started
Tessl Logo

ai-ecoverse/advanced-skills

Quarantined high-risk skills (browser session capture, WebSocket interception) for Slack and Microsoft Teams. Install only after reviewing the security implications.

76

Quality

96%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

slack-ext-connect.test.jsskills/slack/tests/

// Tests for the Slack Connect invite commands in skills/slack/scripts/slack-ext.jsh:
//   guest-invite, connect-revoke, approvals --detail / approvals show.
//
// Run with:
//   tst <path-to-this-file>
//
// Same strategy as slack-ext.test.js: compile the REAL .jsh unmodified (so
// main() dispatches exactly as a user's command line would), inject mock
// sliccy:* modules and a stub browser whose fetch plays the Slack API. Every id,
// name and address here is a fake.

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import test, { is, ok } from 'tst';
import * as _argvMod from '../scripts/argv.js';
import * as _manifestDiffMod from '../scripts/manifest-diff.js';
import * as _gridMod from '../scripts/slack-ext-grid.js';

const SCRIPT = fileURLToPath(new URL('../scripts/slack-ext.jsh', import.meta.url));
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const grid = _gridMod.default || _gridMod;

const ORG = 'E0EXAMPLE01';
const WS = 'T0EXAMPLE01';
const CHAN = 'C0EXAMPLE01';
const INVITE = 'I0EXAMPLE01';
const EMAIL = 'guest@example.com';

class NodeExitError extends Error {
  constructor(message, code) {
    super(message);
    this.name = 'NodeExitError';
    this.exitCode = code !== undefined ? code : 1;
  }
}

// A sharedApprovals.list row as measured, with fakes, INCLUDING icon/avatar
// fields that the detail view must strip.
function approvalRow(over) {
  return Object.assign(
    {
      id: INVITE,
      status: 'pending',
      connection_status: 'pending',
      channel: { id: CHAN, name: 'example-shared' },
      home_user: {
        id: 'U0EXAMPLE01',
        real_name: 'Example Inviter',
        profile: { image_48: 'https://example.invalid/avatar-home.png' },
      },
      away_user: {
        id: 'U0EXAMPLE02',
        real_name: 'Example Guest',
        team_id: 'T0EXAMPLE02',
        profile: { email: EMAIL, image_72: 'https://example.invalid/avatar-away.png' },
      },
      connecting_team: {
        id: 'T0EXAMPLE02',
        name: 'Example Partner Org',
        domain: 'example-partner',
        requires_sponsorship: true,
        icon: { image_68: 'https://example.invalid/icon-team.png' },
      },
      home_date_approve: 1790000000,
      away_date_approve: 0,
      approving_user_id: 'U0EXAMPLE01',
      invite_date_created: 1789900000,
      date_expire: 1791000000,
    },
    over || {}
  );
}

/**
 * @param {object} opts
 * @param {string[]} opts.argv
 * @param {function} [opts.api] (method, params, state) => body | undefined
 * @param {object[]} [opts.rows] initial sharedApprovals rows
 */
async function load(opts) {
  const calls = [];
  const stdout = [];
  const stderr = [];
  const state = { rows: (opts.rows || [approvalRow()]).map((r) => JSON.parse(JSON.stringify(r))) };
  const fakeTab = { id: 'tab1', url: 'https://app.slack.com/client/' + WS + '/' + CHAN };

  const browserStub = {
    async findTab() {
      return fakeTab;
    },
    async localStorage(_tab, key) {
      if (key !== 'localConfig_v2') return null;
      return JSON.stringify({ teams: { [WS]: { token: 'xoxc-test-ws' }, [ORG]: { token: 'xoxc-test-org' } } });
    },
    async fetch(_tab, url, fetchOpts) {
      const method = url.replace('/api/', '');
      const params = {};
      let token = null;
      if (fetchOpts && typeof fetchOpts.body === 'string') {
        for (const [k, v] of new URLSearchParams(fetchOpts.body)) {
          if (k === 'token') token = v;
          else params[k] = v;
        }
      }
      calls.push({ method, params, token });
      if (opts.api) {
        const custom = opts.api(method, params, state);
        if (custom !== undefined) return { body: custom };
      }
      if (method === 'conversations.sharedApprovals.list') {
        return { body: { ok: true, approvals: JSON.parse(JSON.stringify(state.rows)), response_metadata: { next_cursor: '' } } };
      }
      if (method === 'conversations.revokeSharedInvite') {
        const row = state.rows.find((r) => r.id === params.invite_id);
        if (row) {
          row.status = 'expired';
          row.date_expire = 1790500000;
        }
        return { body: { ok: true } };
      }
      if (method === 'users.admin.inviteBulk') {
        const invites = JSON.parse(params.invites);
        return {
          body: {
            ok: true,
            invites: invites.map((i) => ({ email: i.email, ok: true, invite_id: 'I0EXAMPLE02', expiration_ts: 1791000000 })),
          },
        };
      }
      return { body: { ok: false, error: 'not_mocked' } };
    },
  };

  const cliStub = {
    die(message, options) {
      const code = options && options.exitCode !== undefined ? options.exitCode : 1;
      stderr.push(String(message));
      throw new NodeExitError(String(message), code);
    },
    help(message) {
      stdout.push(String(message));
      throw new NodeExitError('help', 0);
    },
    out(value) {
      stdout.push(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
    },
    warn(message) {
      stderr.push(String(message));
    },
  };

  const mocks = {
    'sliccy:browser': browserStub,
    'sliccy:cli': cliStub,
    'sliccy:color': new Proxy({}, { get: () => (s) => String(s) }),
    'sliccy:exec': { exec: async () => ({ exitCode: 0, stdout: '', stderr: '' }) },
    'sliccy:http': {
      client: () => ({
        post: async () => {
          throw new Error('these commands must not use the App Manifest HTTP client');
        },
      }),
    },
    'sliccy:skill': { config: async () => null },
    fs: {
      readFile: async () => {
        throw new Error('ENOENT');
      },
      writeFile: async () => {
        throw new Error('EACCES');
      },
    },
  };
  const relativeModules = {
    './argv.js': () => _argvMod.default || _argvMod,
    './manifest-diff.js': () => _manifestDiffMod.default || _manifestDiffMod,
    './slack-ext-grid.js': () => grid,
  };
  const mockRequire = (id) => {
    if (Object.hasOwn(mocks, id)) return mocks[id];
    const key = id.replace(/^\.\.\/(scripts\/)?/, './');
    if (Object.hasOwn(relativeModules, key)) return relativeModules[key]();
    throw new Error('unexpected require(' + id + ')');
  };
  const mockProcess = {
    argv: ['node', SCRIPT, ...opts.argv],
    env: {},
    exit: (code) => {
      throw new NodeExitError('exit', code);
    },
  };
  const mockConsole = {
    log: (m) => stdout.push(String(m === undefined ? '' : m)),
    error: (m) => stderr.push(String(m === undefined ? '' : m)),
    warn: (m) => stderr.push(String(m === undefined ? '' : m)),
  };

  const source = readFileSync(SCRIPT, 'utf8');
  const factory = new AsyncFunction('require', 'process', 'console', 'setTimeout', source);
  let runError = null;
  try {
    await factory(mockRequire, mockProcess, mockConsole, setTimeout);
  } catch (e) {
    runError = e;
  }
  return {
    runError,
    calls,
    state,
    text: () => stdout.join('\n'),
    errText: () => stderr.join('\n'),
    exit: () => (runError ? (runError.name === 'NodeExitError' ? runError.exitCode : 'THROW:' + runError.message) : 0),
    methods: () => calls.map((c) => c.method).join(','),
    count: (m) => calls.filter((c) => c.method === m).length,
  };
}

const MUTATING = ['users.admin.inviteBulk', 'conversations.revokeSharedInvite'];
const mutations = (h) => h.calls.filter((c) => MUTATING.includes(c.method)).length;

// ── guest-invite ──────────────────────────────────────────────────────────────

test('guest-invite dry run: makes no Slack call at all, shows the payload target', async () => {
  const h = await load({ argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN] });
  is(h.exit(), 0);
  is(h.calls.length, 0);
  is(mutations(h), 0);
  const t = h.text();
  ok(/Dry run — would invite a single-channel guest/.test(t), t);
  ok(t.includes(EMAIL) && t.includes(WS) && t.includes(CHAN), t);
  ok(/No --confirm; nothing changed/.test(t), t);
});

test('guest-invite dry run --json: one JSON document with the exact params, no call', async () => {
  const h = await load({ argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--json'] });
  is(h.exit(), 0);
  is(h.calls.length, 0);
  const doc = JSON.parse(h.text());
  is(doc.dry_run, true);
  is(doc.params.team_id, WS);
});

test('guest-invite --confirm: exactly one inviteBulk with exactly the measured fields, ORG token', async () => {
  const h = await load({ argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--confirm'] });
  is(h.exit(), 0, h.errText());
  is(h.methods(), 'users.admin.inviteBulk');
  const c = h.calls[0];
  is(c.params, {
    team_id: WS,
    invites: JSON.stringify([{ email: EMAIL, type: 'ultra_restricted', mode: 'manual' }]),
    channels: CHAN,
    ultra_restricted: 'true',
    source: 'invite_modal',
    mode: 'manual',
  });
  is(c.token, 'xoxc-test-org');
  const t = h.text();
  ok(/Result:\s+invited/.test(t), t);
  ok(t.includes('I0EXAMPLE02'), t);
  ok(/Expires:\s+\d{4}-\d\d-\d\d \d\d:\d\dZ/.test(t), t);
});

test('guest-invite --confirm: top-level ok:true with a failed per-invite result exits non-zero', async () => {
  const h = await load({
    argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--confirm'],
    api: (m) => (m === 'users.admin.inviteBulk' ? { ok: true, invites: [{ email: EMAIL, ok: false, error: 'already_invited' }] } : undefined),
  });
  is(h.exit(), 1);
  ok(/FAILED: already_invited/.test(h.text()), h.text());
  ok(/1 of 1 invite\(s\) failed/.test(h.errText()), h.errText());
});

test('guest-invite --confirm: ok:true with no per-invite entry is a failure, not success', async () => {
  const h = await load({
    argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--confirm'],
    api: (m) => (m === 'users.admin.inviteBulk' ? { ok: true, invites: [] } : undefined),
  });
  is(h.exit(), 1);
  ok(/missing_from_response/.test(h.text()), h.text());
});

test('guest-invite --confirm --json: failure still exits non-zero', async () => {
  const h = await load({
    argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--confirm', '--json'],
    api: (m) => (m === 'users.admin.inviteBulk' ? { ok: true, invites: [{ email: EMAIL, ok: false, error: 'invalid_email' }] } : undefined),
  });
  is(h.exit(), 1);
  const doc = JSON.parse(h.text());
  is(doc.summary.failed, 1);
});

test('guest-invite without --ws is refused before any call', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--confirm'] });
  is(h.exit(), 1);
  is(h.calls.length, 0);
  ok(/--ws=<TEAM_ID> is required/.test(h.errText()), h.errText());
});

test('guest-invite with the org id as --ws is refused (team_id must be the workspace)', async () => {
  const h = await load({ argv: ['--ws=' + ORG, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=' + CHAN, '--confirm'] });
  is(h.exit(), 1);
  is(h.calls.length, 0);
  ok(/WORKSPACE ID/.test(h.errText()), h.errText());
});

test('guest-invite refuses a channel list and a bad email', async () => {
  const a = await load({ argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', EMAIL, '--channel=C0EXAMPLE01,C0EXAMPLE02', '--confirm'] });
  is(a.exit(), 1);
  is(a.calls.length, 0);
  const b = await load({ argv: ['--ws=' + WS, '--org=' + ORG, 'guest-invite', 'not-an-email', '--channel=' + CHAN, '--confirm'] });
  is(b.exit(), 1);
  is(b.calls.length, 0);
});

// ── connect-revoke ────────────────────────────────────────────────────────────

test('connect-revoke dry run: reads sharedApprovals.list only and shows the invite', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=' + CHAN] });
  is(h.exit(), 0, h.errText());
  is(h.methods(), 'conversations.sharedApprovals.list');
  is(mutations(h), 0);
  const t = h.text();
  ok(/Dry run — would revoke Slack Connect invite/.test(t), t);
  ok(t.includes('Example Partner Org') && t.includes('Example Guest'), t);
  ok(/No --confirm; nothing changed/.test(t), t);
});

test('connect-revoke --confirm: revokes, RE-READS the row and reports the new status', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=' + CHAN, '--confirm'] });
  is(h.exit(), 0, h.errText());
  is(h.methods(), 'conversations.sharedApprovals.list,conversations.revokeSharedInvite,conversations.sharedApprovals.list');
  is(h.calls[1].params, { invite_id: INVITE, channel: CHAN });
  is(h.calls[1].token, 'xoxc-test-org');
  const t = h.text();
  ok(/Status before:\s+pending/.test(t), t);
  ok(/Status now:\s+expired/.test(t), t);
  ok(/Match:\s+yes/.test(t), t);
});

test('connect-revoke --confirm: ok:true but the re-read still shows pending -> exit 3', async () => {
  const h = await load({
    argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=' + CHAN, '--confirm'],
    api: (m) => (m === 'conversations.revokeSharedInvite' ? { ok: true } : undefined),
  });
  is(h.exit(), 3);
  ok(/Revoke sent, NOT confirmed/.test(h.text()), h.text());
  ok(/re-read shows status pending/.test(h.errText()), h.errText());
});

test('connect-revoke --confirm: team_is_restricted -> clear error and org-token hint, no re-read', async () => {
  const h = await load({
    argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=' + CHAN, '--confirm'],
    api: (m) => (m === 'conversations.revokeSharedInvite' ? { ok: false, error: 'team_is_restricted' } : undefined),
  });
  is(h.exit(), 1);
  const e = h.errText();
  ok(/team_is_restricted\. Nothing was revoked/.test(e), e);
  ok(/ORG-level token/.test(e) && /--org=<E id>/.test(e), e);
  is(h.methods(), 'conversations.sharedApprovals.list,conversations.revokeSharedInvite');
});

test('connect-revoke: unknown invite is refused without a revoke call', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'connect-revoke', 'I0EXAMPLE09', '--channel=' + CHAN, '--confirm'] });
  is(h.exit(), 1);
  is(mutations(h), 0);
  ok(/not found in conversations\.sharedApprovals\.list/.test(h.errText()), h.errText());
});

test('connect-revoke: --channel that does not match the row is refused', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=C0EXAMPLE02', '--confirm'] });
  is(h.exit(), 1);
  is(mutations(h), 0);
  ok(/is for channel C0EXAMPLE01, not C0EXAMPLE02/.test(h.errText()), h.errText());
});

test('connect-revoke: already expired is nothing to do, no revoke call', async () => {
  const h = await load({ rows: [approvalRow({ status: 'expired' })], argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=' + CHAN, '--confirm'] });
  is(h.exit(), 0);
  is(mutations(h), 0);
  ok(/already expired; nothing to revoke/.test(h.text()), h.text());
});

test('connect-revoke: finds the row on a later page', async () => {
  let page = 0;
  const h = await load({
    argv: ['--org=' + ORG, 'connect-revoke', INVITE, '--channel=' + CHAN],
    api: (m) => {
      if (m !== 'conversations.sharedApprovals.list') return undefined;
      page += 1;
      if (page === 1) return { ok: true, approvals: [approvalRow({ id: 'I0EXAMPLE05' })], response_metadata: { next_cursor: 'c2' } };
      return { ok: true, approvals: [approvalRow()], response_metadata: { next_cursor: '' } };
    },
  });
  is(h.exit(), 0, h.errText());
  is(h.count('conversations.sharedApprovals.list'), 2);
  is(h.calls[1].params.cursor, 'c2');
});

// ── approvals detail ──────────────────────────────────────────────────────────

test('approvals show: both approval sides, inviter, invitee + org; icons stripped', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'approvals', 'show', INVITE] });
  is(h.exit(), 0, h.errText());
  is(mutations(h), 0);
  const t = h.text();
  ok(/Inviter:\s+Example Inviter \(U0EXAMPLE01\)/.test(t), t);
  ok(/Invitee:\s+Example Guest \(U0EXAMPLE02\) <guest@example\.com>/.test(t), t);
  ok(/Invitee org:\s+Example Partner Org \(T0EXAMPLE02\)/.test(t), t);
  ok(/Our side:\s+approved \d{4}-/.test(t), t);
  ok(/Other org:\s+NOT approved/.test(t), t);
  ok(/Waiting on:\s+other org/.test(t), t);
  ok(/Sponsorship:\s+required/.test(t), t);
  ok(!/example\.invalid|image_|icon/.test(t), t);
});

test('approvals --detail --json: stdout is one JSON document, both sides present, no icon fields', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'approvals', '--detail', '--json'] });
  is(h.exit(), 0, h.errText());
  const raw = h.text();
  const doc = JSON.parse(raw);
  is(doc.total, 1);
  const d = doc.approvals[0];
  is(d.home_approved, 1790000000);
  is(d.away_approved, 0);
  is(d.waiting_on, 'other org');
  is(d.invitee, { id: 'U0EXAMPLE02', real_name: 'Example Guest', email: EMAIL, team_id: 'T0EXAMPLE02' });
  is(d.connecting_team, { id: 'T0EXAMPLE02', name: 'Example Partner Org', domain: 'example-partner', requires_sponsorship: true });
  ok(!/example\.invalid|image_|"icon"/.test(raw), raw);
});

test('approvals --detail (human): prints the detail block per row', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'approvals', '--detail'] });
  is(h.exit(), 0, h.errText());
  ok(/Other org:\s+NOT approved/.test(h.text()), h.text());
});

test('approvals with an unknown subcommand is refused', async () => {
  const h = await load({ argv: ['--org=' + ORG, 'approvals', 'bogus'] });
  is(h.exit(), 1);
  is(h.calls.length, 0);
});

// ── pure helpers ──────────────────────────────────────────────────────────────

test('summarizeApprovalDetail: waiting_on covers all four states', () => {
  const w = (h, a) => grid.summarizeApprovalDetail(approvalRow({ home_date_approve: h, away_date_approve: a })).waiting_on;
  is(w(1, 0), 'other org');
  is(w(0, 1), 'our side');
  is(w(0, 0), 'both sides');
  is(w(1, 1), 'nobody');
});

test('summarizeInviteResults: case-insensitive email match, per-invite ok only', () => {
  const s = grid.summarizeInviteResults({ ok: true, invites: [{ email: 'Guest@Example.com', ok: true, invite_id: 'I0EXAMPLE03' }] }, [EMAIL]);
  is(s.ok, true);
  is(s.failed, 0);
  const f = grid.summarizeInviteResults({ ok: true, invites: [{ email: EMAIL, ok: 'true' }] }, [EMAIL]);
  is(f.ok, false);
});

tile.json