@antasphere/slideless 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +371 -47
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -3042,7 +3042,7 @@ var {
3042
3042
  Help
3043
3043
  } = import_index.default;
3044
3044
 
3045
- // ../../node_modules/.pnpm/@antasphere+cli-core@0.2.0/node_modules/@antasphere/cli-core/dist/config.js
3045
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/config.js
3046
3046
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3047
3047
  import { dirname, join } from "node:path";
3048
3048
  var HUB_TOOL = "hub";
@@ -3109,16 +3109,46 @@ function redactKey(key) {
3109
3109
  return "****";
3110
3110
  return `${key.slice(0, 13)}\u2026${key.slice(-4)}`;
3111
3111
  }
3112
+ function lookupConnectKey(profile, hubProfileName) {
3113
+ return profile?.connectKeys?.[hubProfileName]?.apiKey;
3114
+ }
3115
+ function storeConnectKey(env, tool, profileName, hubProfileName, entry) {
3116
+ const config = loadConfig(env, tool);
3117
+ const profile = config.profiles[profileName] ?? {};
3118
+ config.profiles[profileName] = {
3119
+ ...profile,
3120
+ connectKeys: { ...profile.connectKeys, [hubProfileName]: entry }
3121
+ };
3122
+ if (!config.activeProfile)
3123
+ config.activeProfile = profileName;
3124
+ return saveConfig(env, tool, config);
3125
+ }
3126
+ function removeConnectKey(env, tool, profileName, hubProfileName) {
3127
+ const config = loadConfig(env, tool);
3128
+ const profile = config.profiles[profileName];
3129
+ if (!profile?.connectKeys || !(hubProfileName in profile.connectKeys))
3130
+ return false;
3131
+ const { [hubProfileName]: _dropped, ...rest } = profile.connectKeys;
3132
+ if (Object.keys(rest).length === 0) {
3133
+ const { connectKeys: _empty, ...bare } = profile;
3134
+ config.profiles[profileName] = bare;
3135
+ } else {
3136
+ config.profiles[profileName] = { ...profile, connectKeys: rest };
3137
+ }
3138
+ saveConfig(env, tool, config);
3139
+ return true;
3140
+ }
3112
3141
 
3113
- // ../../node_modules/.pnpm/@antasphere+cli-core@0.2.0/node_modules/@antasphere/cli-core/dist/io.js
3142
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/io.js
3114
3143
  function printJson(io, value) {
3115
3144
  io.out.write(`${JSON.stringify(value, null, 2)}
3116
3145
  `);
3117
3146
  }
3118
3147
 
3119
- // ../../node_modules/.pnpm/@antasphere+cli-core@0.2.0/node_modules/@antasphere/cli-core/dist/resolve.js
3148
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/resolve.js
3120
3149
  var CliUsageError = class extends Error {
3121
3150
  };
3151
+ var DEFAULT_PROFILE = "default";
3122
3152
  function resolveProfile(config, requested, opts = {}) {
3123
3153
  if (requested) {
3124
3154
  const profile2 = config.profiles[requested];
@@ -3142,7 +3172,7 @@ function resolveApiKey(opts) {
3142
3172
  return opts.flag ?? opts.env[opts.envVar] ?? opts.profile?.apiKey;
3143
3173
  }
3144
3174
 
3145
- // ../../node_modules/.pnpm/@antasphere+cli-core@0.2.0/node_modules/@antasphere/cli-core/dist/auth.js
3175
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/auth.js
3146
3176
  var CliAuthError = class extends Error {
3147
3177
  status;
3148
3178
  code;
@@ -3197,7 +3227,179 @@ var CliAuthClient = class {
3197
3227
  }
3198
3228
  };
3199
3229
 
3200
- // ../../node_modules/.pnpm/@antasphere+cli-core@0.2.0/node_modules/@antasphere/cli-core/dist/migrate.js
3230
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/connect.js
3231
+ var CliConnectError = class extends CliAuthError {
3232
+ stage;
3233
+ constructor(stage, status, code, message, details) {
3234
+ super(status, code, message, details);
3235
+ this.stage = stage;
3236
+ this.name = "CliConnectError";
3237
+ }
3238
+ /**
3239
+ * The hub rejected the key itself: invalid/revoked (401), or a key
3240
+ * minted before the `sso:exchange` scope existed (403 insufficient_scope
3241
+ * — old keys simply lack it). The fix is a fresh `antasphere login`.
3242
+ */
3243
+ get needsHubLogin() {
3244
+ return this.stage === "hub" && (this.status === 401 || this.status === 403 && this.code === "insufficient_scope");
3245
+ }
3246
+ /** The tool rejected the JWT (expired/replayed) — run the exchange again. */
3247
+ get needsReExchange() {
3248
+ return this.stage === "tool" && this.status === 401;
3249
+ }
3250
+ };
3251
+ var CliConnectClient = class {
3252
+ hubBaseUrl;
3253
+ toolBaseUrl;
3254
+ fetchImpl;
3255
+ constructor(options) {
3256
+ this.hubBaseUrl = options.hubBaseUrl.replace(/\/+$/, "");
3257
+ this.toolBaseUrl = options.toolBaseUrl.replace(/\/+$/, "");
3258
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
3259
+ }
3260
+ /** Step 1 — the hub mints the 120 s single-org JWT for the tool's resource. */
3261
+ toolToken(hubApiKey, body) {
3262
+ return this.call("hub", `${this.hubBaseUrl}/api/v1/sso/tool-token`, body, hubApiKey);
3263
+ }
3264
+ /**
3265
+ * Step 2 — the tool verifies the JWT (its only credential) and mints its
3266
+ * key. `hubRefreshToken` is the SAME response's offline grant, relayed so
3267
+ * the tool can store it (a grant-less connect is refused for users with
3268
+ * none already stored — `hub_grant_missing`).
3269
+ */
3270
+ cliConnect(token, hubRefreshToken) {
3271
+ return this.call("tool", `${this.toolBaseUrl}/api/v1/sso/cli-connect`, {
3272
+ token,
3273
+ ...hubRefreshToken ? { hubRefreshToken } : {}
3274
+ });
3275
+ }
3276
+ /** The full two-call exchange. The hub key goes to the hub ONLY; the
3277
+ * offline grant goes to the TOOL only — neither is ever persisted here. */
3278
+ async exchange(hubApiKey, body) {
3279
+ const minted = await this.toolToken(hubApiKey, body);
3280
+ const connected = await this.cliConnect(minted.token, minted.hubRefreshToken);
3281
+ return {
3282
+ key: connected.key,
3283
+ apiKey: connected.apiKey,
3284
+ user: connected.user,
3285
+ toolWorkspaceId: connected.workspaceId
3286
+ };
3287
+ }
3288
+ /** Mirrors CliAuthClient's parse: `{ error: { code, message, details } }` envelopes. */
3289
+ async call(stage, url, body, apiKey) {
3290
+ const headers = { "content-type": "application/json" };
3291
+ if (apiKey)
3292
+ headers["authorization"] = `Bearer ${apiKey}`;
3293
+ const res = await this.fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
3294
+ const json = await res.json().catch(() => null);
3295
+ if (!res.ok) {
3296
+ const errBody = json ?? {};
3297
+ throw new CliConnectError(stage, res.status, errBody.error?.code ?? "unknown_error", errBody.error?.message ?? `Request failed with ${res.status}`, errBody.error?.details);
3298
+ }
3299
+ return json;
3300
+ }
3301
+ };
3302
+
3303
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/connect-on-demand.js
3304
+ async function probeToolInstance(toolBaseUrl, fetchImpl) {
3305
+ const f = fetchImpl ?? globalThis.fetch.bind(globalThis);
3306
+ try {
3307
+ const res = await f(`${toolBaseUrl.replace(/\/+$/, "")}/api/v1/instance`, {
3308
+ method: "GET",
3309
+ // Discovery is served cacheable for CLI/MCP clients, but the probe
3310
+ // must see live state (mirrors the tool SDKs' discovery call).
3311
+ cache: "no-store"
3312
+ });
3313
+ if (!res.ok)
3314
+ return null;
3315
+ const info = await res.json().catch(() => null);
3316
+ const methods = info?.auth?.methods;
3317
+ if (!Array.isArray(methods))
3318
+ return null;
3319
+ const authMethods = methods.filter((m) => typeof m === "string");
3320
+ const edition = typeof info?.edition === "string" ? info.edition : void 0;
3321
+ return { cloud: authMethods.includes("antasphere") || edition === "cloud", edition, authMethods };
3322
+ } catch {
3323
+ return null;
3324
+ }
3325
+ }
3326
+ function activeHubProfile(env) {
3327
+ return resolveProfile(loadConfig(env, HUB_TOOL), void 0);
3328
+ }
3329
+ function defaultMissingHubLogin(tool) {
3330
+ const envVar = `${tool.replace(/-/g, "_").toUpperCase()}_API_KEY`;
3331
+ return `This ${tool} instance signs in through the Antasphere hub. Run \`antasphere login\` once, then retry \u2014 or pass --api-key / set ${envVar}.`;
3332
+ }
3333
+ var DEFAULT_HUB_LOGIN_REJECTED = "The stored Antasphere hub credential was rejected \u2014 run `antasphere login` again, then retry.";
3334
+ function defaultConnected(facts) {
3335
+ return `Connected to ${facts.toolBaseUrl} as ${facts.email} via Antasphere` + (facts.stored ? "" : ` \u2014 key NOT cached: profile '${facts.profileName}' points at ${facts.profileBaseUrl}`) + ".\n";
3336
+ }
3337
+ async function connectOnDemand(opts) {
3338
+ assertToolNamespace(opts.tool);
3339
+ const toolBaseUrl = opts.toolBaseUrl.replace(/\/+$/, "");
3340
+ const messages = opts.messages ?? {};
3341
+ const profileName = opts.profileName ?? DEFAULT_PROFILE;
3342
+ const hub = activeHubProfile(opts.env);
3343
+ const existing = loadConfig(opts.env, opts.tool).profiles[profileName];
3344
+ const pinnedHere = existing?.baseUrl?.replace(/\/+$/, "") === toolBaseUrl;
3345
+ if (pinnedHere && hub.name) {
3346
+ const cachedKey = lookupConnectKey(existing, hub.name);
3347
+ if (cachedKey)
3348
+ return { outcome: "connected", key: cachedKey, cached: true };
3349
+ }
3350
+ const probe = await probeToolInstance(toolBaseUrl, opts.fetch);
3351
+ if (!probe?.cloud)
3352
+ return { outcome: "not_cloud" };
3353
+ const hubKey = hub.profile?.apiKey;
3354
+ const hubBaseUrl = hub.profile?.baseUrl;
3355
+ if (!hubKey || !hubBaseUrl || !hub.name) {
3356
+ throw new CliUsageError(messages.missingHubLogin ?? defaultMissingHubLogin(opts.tool));
3357
+ }
3358
+ const resource = opts.resource ?? `${toolBaseUrl}/mcp`;
3359
+ const connect = new CliConnectClient({
3360
+ hubBaseUrl,
3361
+ toolBaseUrl,
3362
+ ...opts.fetch ? { fetch: opts.fetch } : {}
3363
+ });
3364
+ const body = { resource };
3365
+ let result;
3366
+ try {
3367
+ result = await connect.exchange(hubKey, body);
3368
+ } catch (e) {
3369
+ if (e instanceof CliConnectError && e.needsReExchange) {
3370
+ result = await connect.exchange(hubKey, body);
3371
+ } else if (e instanceof CliConnectError && e.needsHubLogin) {
3372
+ throw new CliUsageError(messages.hubLoginRejected ?? DEFAULT_HUB_LOGIN_REJECTED);
3373
+ } else {
3374
+ throw e;
3375
+ }
3376
+ }
3377
+ const stored = !existing?.baseUrl || pinnedHere;
3378
+ if (stored) {
3379
+ storeConnectKey(opts.env, opts.tool, profileName, hub.name, {
3380
+ apiKey: result.key,
3381
+ email: result.user.email,
3382
+ mintedAt: (/* @__PURE__ */ new Date()).toISOString()
3383
+ });
3384
+ if (!existing?.baseUrl) {
3385
+ const cfg = loadConfig(opts.env, opts.tool);
3386
+ cfg.profiles[profileName] = { ...cfg.profiles[profileName], baseUrl: toolBaseUrl };
3387
+ saveConfig(opts.env, opts.tool, cfg);
3388
+ }
3389
+ }
3390
+ const facts = {
3391
+ toolBaseUrl,
3392
+ email: result.user.email,
3393
+ stored,
3394
+ profileName,
3395
+ // `stored` is false only when the profile names another instance.
3396
+ ...stored ? {} : { profileBaseUrl: existing?.baseUrl }
3397
+ };
3398
+ opts.notify?.((messages.connected ?? defaultConnected)(facts));
3399
+ return { outcome: "connected", key: result.key, result, cached: false, stored };
3400
+ }
3401
+
3402
+ // ../../node_modules/.pnpm/@antasphere+cli-core@0.4.0/node_modules/@antasphere/cli-core/dist/migrate.js
3201
3403
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
3202
3404
  import { join as join2 } from "node:path";
3203
3405
  function migrateLegacyConfig(env, spec) {
@@ -3315,6 +3517,14 @@ var PlatformClient = class {
3315
3517
  me() {
3316
3518
  return this.request("GET", "/me");
3317
3519
  }
3520
+ /**
3521
+ * CLOUD EDITION only (404 on oss); sessions only (machines 403): record
3522
+ * the explicit first-run-welcome dismissal — flips /me's
3523
+ * `firstRunPending` to false, permanently. Idempotent.
3524
+ */
3525
+ dismissOnboarding() {
3526
+ return this.request("POST", "/me/onboarding/dismiss");
3527
+ }
3318
3528
  /** Better Auth session probe — null when signed out. */
3319
3529
  async session() {
3320
3530
  return this.request("GET", "/auth/get-session");
@@ -3337,6 +3547,15 @@ var PlatformClient = class {
3337
3547
  cliAuthComplete(req) {
3338
3548
  return this.request("POST", "/cli/auth/complete", req);
3339
3549
  }
3550
+ /**
3551
+ * Revoke the PRESENTING API key (CLI logout). Self-revocation only: the
3552
+ * route names no key, so the credential can kill exactly itself. Requires
3553
+ * an API-key credential with presentations:write (sessions and OAuth
3554
+ * bearers are refused — the dashboard is their key-management surface).
3555
+ */
3556
+ cliAuthRevoke() {
3557
+ return this.request("DELETE", "/cli/auth/key");
3558
+ }
3340
3559
  /**
3341
3560
  * Public, CLOUD EDITION only (404 on oss): exchange a hub-minted 120 s
3342
3561
  * connect JWT (the hub's /sso/tool-token response) for an `slk_` API key
@@ -3346,6 +3565,17 @@ var PlatformClient = class {
3346
3565
  ssoCliConnect(req) {
3347
3566
  return this.request("POST", "/sso/cli-connect", req);
3348
3567
  }
3568
+ /**
3569
+ * CLOUD EDITION only (404 on oss); sessions only (machines 403 — the
3570
+ * path is unlisted in the scope allowlist): single logout. The server
3571
+ * revokes the local session and clears the SSO hint cookie in this very
3572
+ * response; `url` is the hub end-session URL the browser must then VISIT
3573
+ * to end the hub anchor session (null = local signout only — go straight
3574
+ * to /login?signed_out=1).
3575
+ */
3576
+ ssoLogout() {
3577
+ return this.request("POST", "/sso/logout");
3578
+ }
3349
3579
  // ── Members ───────────────────────────────────────────────────────────────
3350
3580
  members(params = {}) {
3351
3581
  return this.request("GET", this.pathWithQuery("/members", params));
@@ -3418,15 +3648,6 @@ var PlatformClient = class {
3418
3648
  acceptInvitation(req) {
3419
3649
  return this.request("POST", "/invitations/accept", req);
3420
3650
  }
3421
- // ── OAuth consent ─────────────────────────────────────────────────────────
3422
- /**
3423
- * Park the workspace the upcoming OAuth consent should bind (ADR 014).
3424
- * Sessions only; the consent page calls it right before approving when
3425
- * the user picked a non-default workspace.
3426
- */
3427
- oauthConsentWorkspace(workspaceId) {
3428
- return this.request("POST", "/oauth/consent-workspace", { workspaceId });
3429
- }
3430
3651
  // ── Audit ─────────────────────────────────────────────────────────────────
3431
3652
  audit(params = {}) {
3432
3653
  return this.request("GET", this.pathWithQuery("/audit", params));
@@ -3657,6 +3878,9 @@ function saveConfig2(env, config) {
3657
3878
  function clearConfig2(env) {
3658
3879
  clearConfig(env, TOOL);
3659
3880
  }
3881
+ function removeConnectKey2(env, profileName, hubProfileName) {
3882
+ return removeConnectKey(env, TOOL, profileName, hubProfileName);
3883
+ }
3660
3884
 
3661
3885
  // src/context.ts
3662
3886
  function resolveProfile2(config, requested) {
@@ -3676,7 +3900,11 @@ function resolveContext(cmd, io) {
3676
3900
  profile,
3677
3901
  missingMessage: "No instance configured. Pass --api-url <url>, set SLIDELESS_URL, or sign in once with `slideless auth login-request --api-url <url> --email <you>` to save a profile."
3678
3902
  });
3679
- const apiKey = resolveApiKey({ flag: opts.apiKey, env: io.env, envVar: "SLIDELESS_API_KEY", profile });
3903
+ let apiKey = resolveApiKey({ flag: opts.apiKey, env: io.env, envVar: "SLIDELESS_API_KEY", profile });
3904
+ if (!apiKey && profile?.baseUrl?.replace(/\/+$/, "") === baseUrl) {
3905
+ const hub = activeHubProfile(io.env);
3906
+ if (hub.name) apiKey = lookupConnectKey(profile, hub.name);
3907
+ }
3680
3908
  const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
3681
3909
  return {
3682
3910
  client: new PlatformClient({ baseUrl, ...apiKey ? { apiKey } : {}, fetch: fetchImpl }),
@@ -3688,13 +3916,29 @@ function resolveContext(cmd, io) {
3688
3916
  config
3689
3917
  };
3690
3918
  }
3691
- function requireApiKey(ctx) {
3692
- if (!ctx.apiKey) {
3693
- throw new CliUsageError(
3694
- "An API key is required. Sign in (`slideless auth login-request --email <you>`), paste one (`slideless login`), pass --api-key, or set SLIDELESS_API_KEY."
3695
- );
3919
+ var NO_KEY_MESSAGE = "An API key is required. Sign in (`slideless auth login-request --email <you>`), paste one (`slideless login`), pass --api-key, or set SLIDELESS_API_KEY.";
3920
+ var MISSING_HUB_LOGIN_MESSAGE = "This Slideless instance signs in through the Antasphere hub. Run `antasphere login` once, then retry \u2014 or pass --api-key <slk_\u2026> / set SLIDELESS_API_KEY.";
3921
+ async function requireApiKey(ctx) {
3922
+ if (ctx.apiKey) return ctx.apiKey;
3923
+ const { io } = ctx;
3924
+ const outcome = await connectOnDemand({
3925
+ tool: "slideless",
3926
+ toolBaseUrl: ctx.baseUrl,
3927
+ ...ctx.profileName !== void 0 ? { profileName: ctx.profileName } : {},
3928
+ env: io.env,
3929
+ // Thread the injected fetch so the probe + exchange stay on the test
3930
+ // harness wire (and any proxying the runner set up).
3931
+ ...io.fetch ? { fetch: io.fetch } : {},
3932
+ notify: (line) => io.err.write(line),
3933
+ messages: { missingHubLogin: MISSING_HUB_LOGIN_MESSAGE }
3934
+ });
3935
+ if (outcome.outcome === "not_cloud") {
3936
+ throw new CliUsageError(NO_KEY_MESSAGE);
3696
3937
  }
3697
- return ctx.apiKey;
3938
+ ctx.apiKey = outcome.key;
3939
+ const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
3940
+ ctx.client = new PlatformClient({ baseUrl: ctx.baseUrl, apiKey: outcome.key, fetch: fetchImpl });
3941
+ return outcome.key;
3698
3942
  }
3699
3943
  function table(rows) {
3700
3944
  if (rows.length === 0) return "";
@@ -3730,10 +3974,26 @@ async function readLineFromStdin() {
3730
3974
  for await (const chunk of process.stdin) chunks.push(chunk);
3731
3975
  return Buffer.concat(chunks).toString("utf8").split("\n")[0]?.trim() ?? "";
3732
3976
  }
3977
+ async function refuseOtpLoginOnCloud(baseUrl, io) {
3978
+ let cloud = false;
3979
+ try {
3980
+ const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
3981
+ const info = await new PlatformClient({ baseUrl, fetch: fetchImpl }).instance();
3982
+ cloud = info.auth.methods.includes("antasphere") || info.edition === "cloud";
3983
+ } catch {
3984
+ return;
3985
+ }
3986
+ if (cloud) {
3987
+ throw new CliUsageError(
3988
+ `${baseUrl} is an Antasphere-cloud instance \u2014 it signs in at the hub, not with its own email codes. Run \`antasphere login\` once; \`slideless\` then connects automatically (or pass --api-key <slk_\u2026> / set SLIDELESS_API_KEY).`
3989
+ );
3990
+ }
3991
+ }
3733
3992
  function registerAuthCommands(program2, io) {
3734
3993
  const auth = program2.command("auth").description("Sign in over email OTP (mints an API key)");
3735
3994
  auth.command("login-request").description("Email a 6-digit sign-in code (existing accounts only \u2014 sign-up stays closed)").requiredOption("--email <email>", "account email on the instance").action(async (opts, cmd) => {
3736
3995
  const baseUrl = resolveAuthUrl(cmd, io);
3996
+ await refuseOtpLoginOnCloud(baseUrl, io);
3737
3997
  const client = new CliAuthClient({ baseUrl, ...io.fetch ? { fetch: io.fetch } : {} });
3738
3998
  const result = await client.request({ email: opts.email });
3739
3999
  const json = Boolean(cmd.optsWithGlobals().json);
@@ -3752,6 +4012,7 @@ Complete with: slideless auth login-complete --email ${opts.email} --code <code>
3752
4012
  async (opts, cmd) => {
3753
4013
  const globals = cmd.optsWithGlobals();
3754
4014
  const baseUrl = resolveAuthUrl(cmd, io);
4015
+ await refuseOtpLoginOnCloud(baseUrl, io);
3755
4016
  const client = new CliAuthClient({ baseUrl, ...io.fetch ? { fetch: io.fetch } : {} });
3756
4017
  const result = await client.complete({
3757
4018
  email: opts.email,
@@ -3800,7 +4061,9 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3800
4061
  `
3801
4062
  );
3802
4063
  });
3803
- program2.command("logout").description("Forget the stored API key of the active (or named) profile").action(async (_opts, cmd) => {
4064
+ program2.command("logout").description(
4065
+ "Forget the stored API key of the active (or named) profile; a hub-connected profile disconnects from Antasphere (revokes + evicts the cached user-scoped key)"
4066
+ ).action(async (_opts, cmd) => {
3804
4067
  const globals = cmd.optsWithGlobals();
3805
4068
  const config2 = loadConfig2(io.env);
3806
4069
  const profileName = globals.profile ?? config2.activeProfile;
@@ -3808,6 +4071,50 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3808
4071
  if (!profileName || !profile) {
3809
4072
  throw new CliUsageError("No profile to log out of.");
3810
4073
  }
4074
+ const connectKeys = profile.connectKeys ?? {};
4075
+ const slots = Object.entries(connectKeys);
4076
+ if (!profile.apiKey && slots.length > 0) {
4077
+ const rawUrl = profile.baseUrl;
4078
+ let revoked = true;
4079
+ for (const [slot, entry] of slots) {
4080
+ if (rawUrl) {
4081
+ const baseUrl = rawUrl.replace(/\/+$/, "");
4082
+ try {
4083
+ const client = new CliAuthClient({ baseUrl, ...io.fetch ? { fetch: io.fetch } : {} });
4084
+ await client.revoke(entry.apiKey);
4085
+ } catch (e) {
4086
+ revoked = false;
4087
+ if (e instanceof CliAuthError && e.status === 401) {
4088
+ io.err.write("The cached key was already unusable \u2014 evicting it anyway.\n");
4089
+ } else if (e instanceof CliAuthError && e.status === 403) {
4090
+ io.err.write(
4091
+ `This instance refused the self-revoke (${e.code}) \u2014 the key STAYS VALID server-side; revoke it from the dashboard. Evicting the cached copy.
4092
+ `
4093
+ );
4094
+ } else {
4095
+ throw e;
4096
+ }
4097
+ }
4098
+ } else {
4099
+ revoked = false;
4100
+ io.err.write("No instance URL known for this profile \u2014 evicting the cached key locally only.\n");
4101
+ }
4102
+ removeConnectKey2(io.env, profileName, slot);
4103
+ }
4104
+ if (globals.json) {
4105
+ return printJson(io, {
4106
+ profile: profileName,
4107
+ hubProfiles: slots.map(([slot]) => slot),
4108
+ revoked,
4109
+ evicted: true
4110
+ });
4111
+ }
4112
+ io.out.write(
4113
+ `Disconnected from Antasphere on profile "${profileName}"${revoked ? " (key revoked server-side)" : ""}.
4114
+ `
4115
+ );
4116
+ return;
4117
+ }
3811
4118
  delete profile.apiKey;
3812
4119
  if (!profile.baseUrl) delete config2.profiles[profileName];
3813
4120
  saveConfig2(io.env, config2);
@@ -3819,14 +4126,14 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3819
4126
  });
3820
4127
  program2.command("whoami").description("Show the identity behind the resolved API key").action(async (_opts, cmd) => {
3821
4128
  const ctx = resolveContext(cmd, io);
3822
- requireApiKey(ctx);
4129
+ await requireApiKey(ctx);
3823
4130
  const me = await ctx.client.me();
3824
4131
  if (ctx.json) return printJson(io, me);
3825
4132
  io.out.write(
3826
4133
  `${me.user.name} <${me.user.email}>
3827
4134
  instance: ${ctx.baseUrl}
3828
- workspace: ${me.workspace.name}
3829
- role: ${me.role} (via ${me.via})
4135
+ workspace: ${me.workspace?.name ?? "(none)"}
4136
+ role: ${me.role ?? "(none)"} (via ${me.via})
3830
4137
  scopes: ${me.scopes ? me.scopes.join(", ") : "full (session)"}
3831
4138
  ` + (me.via === "api_key" ? ` key expires: ${me.apiKeyExpiresAt ?? "never"}
3832
4139
  ` : "")
@@ -3834,7 +4141,7 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3834
4141
  });
3835
4142
  program2.command("verify").description("Check that the resolved instance + key work (exit 0 = ok)").action(async (_opts, cmd) => {
3836
4143
  const ctx = resolveContext(cmd, io);
3837
- requireApiKey(ctx);
4144
+ await requireApiKey(ctx);
3838
4145
  const me = await ctx.client.me();
3839
4146
  if (ctx.json) return printJson(io, { ok: true, baseUrl: ctx.baseUrl, user: me.user });
3840
4147
  io.out.write(`ok \u2014 ${me.user.email} (${me.role}) @ ${ctx.baseUrl}
@@ -3862,7 +4169,16 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3862
4169
  profiles: Object.fromEntries(
3863
4170
  names.map((n) => {
3864
4171
  const p = config2.profiles[n];
3865
- return [n, { baseUrl: p.baseUrl ?? null, apiKey: p.apiKey ? redactKey(p.apiKey) : null }];
4172
+ return [
4173
+ n,
4174
+ {
4175
+ baseUrl: p.baseUrl ?? null,
4176
+ apiKey: p.apiKey ? redactKey(p.apiKey) : null,
4177
+ // Hub profiles this tool profile is connected through
4178
+ // (one user-scoped key each, valid for every org).
4179
+ hubProfiles: Object.keys(p.connectKeys ?? {})
4180
+ }
4181
+ ];
3866
4182
  })
3867
4183
  )
3868
4184
  });
@@ -3877,11 +4193,12 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3877
4193
  table(
3878
4194
  names.map((n) => {
3879
4195
  const p = config2.profiles[n];
4196
+ const hubProfiles = Object.keys(p.connectKeys ?? {});
3880
4197
  return [
3881
4198
  config2.activeProfile === n ? "*" : " ",
3882
4199
  n,
3883
4200
  p.baseUrl ?? "(no url)",
3884
- p.apiKey ? redactKey(p.apiKey) : "(no key)"
4201
+ p.apiKey ? redactKey(p.apiKey) : hubProfiles.length > 0 ? `(hub: ${hubProfiles.join(", ")})` : "(no key)"
3885
4202
  ];
3886
4203
  })
3887
4204
  )
@@ -3898,7 +4215,14 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3898
4215
  profiles: Object.fromEntries(
3899
4216
  Object.entries(cfg.profiles).map(([n, p]) => [
3900
4217
  n,
3901
- { baseUrl: p.baseUrl ?? null, apiKey: p.apiKey ? redactKey(p.apiKey) : null }
4218
+ {
4219
+ baseUrl: p.baseUrl ?? null,
4220
+ apiKey: p.apiKey ? redactKey(p.apiKey) : null,
4221
+ // Hub-connected keys (one per hub profile), redacted like the rest.
4222
+ connectKeys: Object.fromEntries(
4223
+ Object.entries(p.connectKeys ?? {}).map(([slot, entry]) => [slot, redactKey(entry.apiKey)])
4224
+ )
4225
+ }
3902
4226
  ])
3903
4227
  )
3904
4228
  };
@@ -3920,7 +4244,7 @@ API key "${result.apiKey.name}" (${redactKey(result.key)}) saved to profile "${p
3920
4244
  function registerDeckCommands(program2, io) {
3921
4245
  program2.command("list").description("List presentations (newest first, cursor-paginated)").option("--cursor <cursor>", "resume from a previous nextCursor").option("--limit <n>", "page size (1-100)", (v) => parseInt(v, 10)).option("--all", "follow nextCursor until every page is fetched", false).action(async (opts, cmd) => {
3922
4246
  const ctx = resolveContext(cmd, io);
3923
- requireApiKey(ctx);
4247
+ await requireApiKey(ctx);
3924
4248
  const params = {};
3925
4249
  if (opts.cursor) params.cursor = opts.cursor;
3926
4250
  if (opts.limit !== void 0) params.limit = opts.limit;
@@ -3948,7 +4272,7 @@ function registerDeckCommands(program2, io) {
3948
4272
  });
3949
4273
  program2.command("get <id>").description("Show one presentation (metadata + latest version)").action(async (id, _opts, cmd) => {
3950
4274
  const ctx = resolveContext(cmd, io);
3951
- requireApiKey(ctx);
4275
+ await requireApiKey(ctx);
3952
4276
  const deck = await ctx.client.presentation(id);
3953
4277
  if (ctx.json) return printJson(io, deck);
3954
4278
  io.out.write(
@@ -3965,7 +4289,7 @@ function registerDeckCommands(program2, io) {
3965
4289
  });
3966
4290
  program2.command("delete <id>").description("Delete a presentation (soft delete; versions and share links stop resolving)").action(async (id, _opts, cmd) => {
3967
4291
  const ctx = resolveContext(cmd, io);
3968
- requireApiKey(ctx);
4292
+ await requireApiKey(ctx);
3969
4293
  const deck = await ctx.client.deletePresentation(id);
3970
4294
  if (ctx.json) return printJson(io, deck);
3971
4295
  io.out.write(`Deleted "${deck.title}" (${deck.id}).
@@ -4316,7 +4640,7 @@ function registerContentCommands(program2, io) {
4316
4640
  ).option("--title <title>", "deck title (default: existing title, or the folder name)").option("--entry <path>", "entry document (default: index.html, or the only .html)").option("--kind <kind>", "presentation | app | plan (new decks only)", "presentation").option("--interactive", "mark the deck as embedding interactive content (new decks only)", false).option("--id <deckId>", "push a new version of this existing deck").option("--new", `force a NEW deck even when ${LINK_FILENAME} links one`, false).action(
4317
4641
  async (path, opts, cmd) => {
4318
4642
  const ctx = resolveContext(cmd, io);
4319
- requireApiKey(ctx);
4643
+ await requireApiKey(ctx);
4320
4644
  const target = path ?? ".";
4321
4645
  const scan = await scanDeck(target);
4322
4646
  const entryPath = detectEntry(scan, opts.entry);
@@ -4392,7 +4716,7 @@ function registerContentCommands(program2, io) {
4392
4716
  `Download a deck version to a folder (id defaults to the ${LINK_FILENAME} link in the target)`
4393
4717
  ).option("--at <version>", "pull this version instead of the latest", (v) => parseInt(v, 10)).action(async (id, path, opts, cmd) => {
4394
4718
  const ctx = resolveContext(cmd, io);
4395
- requireApiKey(ctx);
4719
+ await requireApiKey(ctx);
4396
4720
  let deckId = id ?? null;
4397
4721
  let dest = path ?? null;
4398
4722
  if (!deckId) {
@@ -4437,7 +4761,7 @@ function registerContentCommands(program2, io) {
4437
4761
  program2.command("pull-annotations [id]").description(`List a deck's annotations (id defaults to the ${LINK_FILENAME} link in .)`).option("--version <n>", "only notes anchored to this version", (v) => parseInt(v, 10)).option("--status <status>", "open | resolved").option("--out <file>", "write the annotations as JSON to this file").action(
4438
4762
  async (id, opts, cmd) => {
4439
4763
  const ctx = resolveContext(cmd, io);
4440
- requireApiKey(ctx);
4764
+ await requireApiKey(ctx);
4441
4765
  let deckId = id ?? null;
4442
4766
  if (!deckId) {
4443
4767
  const link = await readLink(resolve3("."));
@@ -4550,7 +4874,7 @@ function registerSharingCommands(program2, io) {
4550
4874
  program2.command("share <id>").description("Create a per-recipient share link (prints the viewer URL \u2014 shown once)").option("--name <name>", "owner-facing recipient label", "cli").option("--to-version <n>", "pin the recipient to this version", (v) => parseInt(v, 10)).option("--annotator", "let the recipient annotate", false).option("--expires <datetime>", "ISO expiry, e.g. 2026-12-31T23:59:59Z").option("--password <password>", "viewer password (min 4 chars)").action(
4551
4875
  async (id, opts, cmd) => {
4552
4876
  const ctx = resolveContext(cmd, io);
4553
- requireApiKey(ctx);
4877
+ await requireApiKey(ctx);
4554
4878
  const created = await ctx.client.createShareToken(id, shareOptionsOf(opts));
4555
4879
  if (ctx.json) return printJson(io, created);
4556
4880
  io.out.write(
@@ -4563,7 +4887,7 @@ function registerSharingCommands(program2, io) {
4563
4887
  );
4564
4888
  program2.command("unshare <id>").description("Revoke one share token (--token) or ALL active tokens of the deck").option("--token <tokenId>", "revoke only this token").action(async (id, opts, cmd) => {
4565
4889
  const ctx = resolveContext(cmd, io);
4566
- requireApiKey(ctx);
4890
+ await requireApiKey(ctx);
4567
4891
  if (opts.token) {
4568
4892
  const revoked2 = await ctx.client.revokeShareToken(id, opts.token);
4569
4893
  if (ctx.json) return printJson(io, revoked2);
@@ -4592,7 +4916,7 @@ function registerSharingCommands(program2, io) {
4592
4916
  program2.command("share-email <id>").description("Create a personal share link per recipient and email it (one token per address)").requiredOption("--to <email...>", "recipient email(s)").option("--to-version <n>", "pin recipients to this version", (v) => parseInt(v, 10)).option("--annotator", "let recipients annotate", false).option("--expires <datetime>", "ISO expiry").option("--password <password>", "viewer password (tell recipients separately)").option("--message <text>", "personal note included in the email").action(
4593
4917
  async (id, opts, cmd) => {
4594
4918
  const ctx = resolveContext(cmd, io);
4595
- requireApiKey(ctx);
4919
+ await requireApiKey(ctx);
4596
4920
  const results = [];
4597
4921
  for (const email of opts.to) {
4598
4922
  const created = await ctx.client.createShareToken(id, shareOptionsOf({ ...opts, name: email }));
@@ -4611,7 +4935,7 @@ function registerSharingCommands(program2, io) {
4611
4935
  program2.command("pin <id> <tokenId>").description("Pin a share token to a version, or set it back to following the latest").option("--to-version <n>", "freeze the recipient on this version", (v) => parseInt(v, 10)).option("--latest", "follow the latest version again", false).action(
4612
4936
  async (id, tokenId, opts, cmd) => {
4613
4937
  const ctx = resolveContext(cmd, io);
4614
- requireApiKey(ctx);
4938
+ await requireApiKey(ctx);
4615
4939
  if (opts.latest === (opts.toVersion !== void 0)) {
4616
4940
  throw new CliUsageError("Pass exactly one of --to-version <n> or --latest.");
4617
4941
  }
@@ -4627,7 +4951,7 @@ function registerSharingCommands(program2, io) {
4627
4951
  );
4628
4952
  program2.command("invite <id>").description("Invite a dev collaborator to one deck (prints the claim link)").requiredOption("--email <email>", "invitee email").action(async (id, opts, cmd) => {
4629
4953
  const ctx = resolveContext(cmd, io);
4630
- requireApiKey(ctx);
4954
+ await requireApiKey(ctx);
4631
4955
  const invited = await ctx.client.inviteCollaborator(id, { email: opts.email });
4632
4956
  if (ctx.json) return printJson(io, invited);
4633
4957
  io.out.write(
@@ -4639,7 +4963,7 @@ function registerSharingCommands(program2, io) {
4639
4963
  });
4640
4964
  program2.command("uninvite <id> <collaboratorId>").description("Revoke a collaborator grant (pending or active)").action(async (id, collaboratorId, _opts, cmd) => {
4641
4965
  const ctx = resolveContext(cmd, io);
4642
- requireApiKey(ctx);
4966
+ await requireApiKey(ctx);
4643
4967
  const revoked = await ctx.client.removeCollaborator(id, collaboratorId);
4644
4968
  if (ctx.json) return printJson(io, revoked);
4645
4969
  io.out.write(`Revoked collaborator grant ${revoked.id} (${revoked.email}).
@@ -4672,7 +4996,7 @@ function registerFileCommands(program2, io) {
4672
4996
  });
4673
4997
  program2.command("export").description("Download the full workspace export as a zip (key needs data:export)").option("-o, --out <path>", "write to this path (defaults to export-<date>.zip)").action(async (opts, cmd) => {
4674
4998
  const ctx = resolveContext(cmd, io);
4675
- requireApiKey(ctx);
4999
+ await requireApiKey(ctx);
4676
5000
  const res = await ctx.client.downloadExport();
4677
5001
  if (!res.body) {
4678
5002
  throw new Error("The export response carried no body");
@@ -4686,7 +5010,7 @@ function registerFileCommands(program2, io) {
4686
5010
  const files = program2.command("files").description("Manage workspace files");
4687
5011
  files.command("list").description("List files in the workspace (newest first, cursor-paginated)").option("--cursor <cursor>", "resume from a previous nextCursor").option("--limit <n>", "page size (1-100)", (v) => parseInt(v, 10)).option("--all", "follow nextCursor until every page is fetched", false).action(async (opts, cmd) => {
4688
5012
  const ctx = resolveContext(cmd, io);
4689
- requireApiKey(ctx);
5013
+ await requireApiKey(ctx);
4690
5014
  const params = {};
4691
5015
  if (opts.cursor) params.cursor = opts.cursor;
4692
5016
  if (opts.limit !== void 0) params.limit = opts.limit;
@@ -4717,7 +5041,7 @@ function registerFileCommands(program2, io) {
4717
5041
  });
4718
5042
  files.command("upload <path>").description("Upload a local file").option("--name <name>", "stored file name (defaults to the local basename)").option("--content-type <type>", "content type", "application/octet-stream").action(async (path, opts, cmd) => {
4719
5043
  const ctx = resolveContext(cmd, io);
4720
- requireApiKey(ctx);
5044
+ await requireApiKey(ctx);
4721
5045
  const bytes = await readFile4(path);
4722
5046
  const name = opts.name ?? basename2(path);
4723
5047
  const result = await ctx.client.uploadFile(name, new Uint8Array(bytes), opts.contentType);
@@ -4730,7 +5054,7 @@ function registerFileCommands(program2, io) {
4730
5054
  });
4731
5055
  files.command("download <id>").description("Download a file by id").option("--out <path>", "write to this path (defaults to the stored name)").action(async (id, opts, cmd) => {
4732
5056
  const ctx = resolveContext(cmd, io);
4733
- const apiKey = requireApiKey(ctx);
5057
+ const apiKey = await requireApiKey(ctx);
4734
5058
  const meta = await ctx.client.file(id);
4735
5059
  const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
4736
5060
  const res = await fetchImpl(ctx.client.fileContentUrl(id), {
@@ -4747,7 +5071,7 @@ function registerFileCommands(program2, io) {
4747
5071
  });
4748
5072
  files.command("rm <id>").description("Delete a file by id").action(async (id, _opts, cmd) => {
4749
5073
  const ctx = resolveContext(cmd, io);
4750
- requireApiKey(ctx);
5074
+ await requireApiKey(ctx);
4751
5075
  await ctx.client.deleteFile(id);
4752
5076
  if (ctx.json) return printJson(io, { deleted: id });
4753
5077
  io.out.write(`Deleted ${id}
@@ -4833,7 +5157,7 @@ function registerCompletionCommand(program2, io) {
4833
5157
  }
4834
5158
 
4835
5159
  // src/index.ts
4836
- var VERSION = "0.2.1";
5160
+ var VERSION = "0.2.4";
4837
5161
  function buildProgram(io) {
4838
5162
  const program2 = new Command();
4839
5163
  program2.name("slideless").description("Command-line client for a Slideless instance (push, share, pull, preview)").version(VERSION).option("--api-url <url>", "instance base URL (or SLIDELESS_URL / profile baseUrl)").option("--url <url>", "alias of --api-url").option("--api-key <key>", "API key (or SLIDELESS_API_KEY / profile apiKey)").option("--profile <name>", "use this saved profile instead of the active one").option("--json", "machine-readable JSON output", false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antasphere/slideless",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "description": "The Slideless CLI — push, share, pull, and preview decks on a Slideless instance",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "test": "vitest run --passWithNoTests"
31
31
  },
32
32
  "devDependencies": {
33
- "@antasphere/cli-core": "^0.2.0",
33
+ "@antasphere/cli-core": "^0.4.0",
34
34
  "@slideless/contract": "workspace:*",
35
35
  "@slideless/sdk": "workspace:*",
36
36
  "@types/node": "^22.15.0",