@byokit/openclaw 0.11.1 → 0.12.0

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.
package/dist/kit.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
+ import { isDeepStrictEqual } from 'node:util';
4
5
  import { fileURLToPath } from 'node:url';
5
- import { PROTOCOL_VERSION } from "./constants.js";
6
+ import { PROTOCOL_VERSION, LIVE_CATALOGS } from "./constants.js";
6
7
  import { atomic } from "./engine-patches.js";
7
8
  import { Approvals } from "./approvals.js";
8
9
  import { Bridge } from "./bridge.js";
@@ -10,15 +11,16 @@ import { Engine, refusedOnly } from "./engine.js";
10
11
  import { AuthStoreSealSizeError } from "./auth-store.js";
11
12
  import { gatewayTransport } from "./transport.js";
12
13
  import { createMembers } from "./members.js";
13
- import { createAccounts } from "./accounts.js";
14
+ import { createAccounts, memberOfAccount } from "./accounts.js";
14
15
  import { createLocks } from "./locks.js";
15
16
  import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
16
17
  import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
17
18
  import { createRuns, signedInProviders } from "./runs.js";
18
- import { createAuthStatus } from "./auth-status.js";
19
+ import { authStatus, createAuthStatus } from "./auth-status.js";
19
20
  import { outputSchema } from "./output.js";
20
21
  import { createKeys } from "./keys.js";
21
22
  import { routes as routeTable } from "./routes.js";
23
+ import { providerOf } from "./pick.js";
22
24
  import { signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
23
25
  import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
24
26
  import { createBrowserHost } from "./browser/host.js";
@@ -26,6 +28,7 @@ import { fileSignInStore } from "./browser/store.js";
26
28
  import { launchBroker } from "./browser/broker.js";
27
29
  /** The engine's learning-mode config key (5.6 "Engine learning state"). */
28
30
  const LEARNING_KEY = 'skills.workshop.autonomous.mode';
31
+ const RESTART_MS = 60_000;
29
32
  const operatorMethods = new Set([
30
33
  'agent',
31
34
  'agent.identity.get',
@@ -443,9 +446,12 @@ export class OpenClawKit {
443
446
  listeners = new Set();
444
447
  off = [];
445
448
  signInDisconnects = new Set();
449
+ phaseListeners = new Set();
446
450
  starting;
447
451
  stopping = false;
448
452
  failures = 0;
453
+ swept = false;
454
+ catalogs = Promise.resolve();
449
455
  browser;
450
456
  browserHost;
451
457
  brokers = new Map();
@@ -657,13 +663,17 @@ export class OpenClawKit {
657
663
  onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
658
664
  this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
659
665
  this.keys = createKeys({ root: this.engine.root,
660
- restarting: () => this.current.phase === 'restarting' || this.current.phase === 'starting',
666
+ prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
667
+ restarting: () => this.engineRestarting(),
661
668
  request: (method, params, options) => this.request()(method, params, options),
662
- ensure: (member) => this.ensureMember(member) });
669
+ ensure: (member) => this.ensureMember(member),
670
+ authConfig: () => this.authSnapshot(),
671
+ restarted: (authBefore, signal) => this.restarted(authBefore, signal) });
663
672
  const slot = {};
664
673
  this.approvalsCtl = new Approvals({
665
674
  request: (method, params, co) => this.request()(method, params, co),
666
675
  bridge: { resolveAsk: (id, d) => slot.bridge?.resolveAsk(id, d) ?? false },
676
+ memberOf: (ref) => this.memberOf(ref),
667
677
  });
668
678
  const host = o.browser ? {
669
679
  gate: async (run, tool, input, info) => {
@@ -692,6 +702,7 @@ export class OpenClawKit {
692
702
  return browserSessionMayRun(this.browserHost.signIns(), key, this.bridge.isRegisteredRun(key, runId));
693
703
  } } : {}),
694
704
  permitted: o.permitted ?? (() => true), approvalTimeoutMs: o.approvalTimeoutMs ?? 180_000,
705
+ memberOf: (ref) => this.memberOf(ref),
695
706
  onAsk: (a) => this.approvalsCtl.add(a), onAskGone: (id) => this.approvalsCtl.remove(id) });
696
707
  slot.bridge = this.bridge;
697
708
  }
@@ -700,6 +711,8 @@ export class OpenClawKit {
700
711
  this.admission?.invalidate();
701
712
  this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
702
713
  this.o.onState?.(this.current);
714
+ for (const fn of this.phaseListeners)
715
+ fn();
703
716
  }
704
717
  get state() { return this.current; }
705
718
  async prepare() {
@@ -746,15 +759,22 @@ export class OpenClawKit {
746
759
  if (!operatorMethods.has(name) && !nodeMethods.has(name))
747
760
  this.o.log?.(`gateway has unknown method: ${name}`);
748
761
  this.members = createMembers({ request: transport.request.bind(transport), root: this.engine.root });
749
- this.accountsCtl = createAccounts({ request: transport.request.bind(transport), root: this.engine.root,
762
+ // An add outlives a restart (activation restarts the gateway before it settles): requests use the kit's current transport.
763
+ this.accountsCtl = createAccounts({ request: (method, params, o) => this.request()(method, params, o), root: this.engine.root,
750
764
  ensure: (member) => this.ensureMember(member), locks: this.locks,
751
765
  signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
752
- log: this.o.log });
766
+ routes: this.routeMap, log: this.o.log });
753
767
  // The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
754
768
  await this.bridge.start();
755
769
  this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
756
770
  this.failures = 0;
757
771
  this.setState({ phase: 'ready' });
772
+ this.syncCatalogs();
773
+ // A failed key add whose abandon clear never reached the engine leaves an unsealed key with no ready marker;
774
+ // runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive.
775
+ if (!this.swept)
776
+ void this.keys.sweep().then((done) => { if (done)
777
+ this.swept = true; }).catch(() => undefined);
758
778
  if (this.o.browser)
759
779
  await this.checkBrowserTools();
760
780
  // Replays the engine's native approval lists over the now-live transport (N9).
@@ -803,6 +823,39 @@ export class OpenClawKit {
803
823
  this.bridge.stop();
804
824
  await transport?.stop();
805
825
  }
826
+ engineRestarting() {
827
+ return this.current.phase === 'restarting' || this.current.phase === 'starting';
828
+ }
829
+ authSnapshot() {
830
+ return JSON.stringify(this.getConfigKey('auth')) ?? '';
831
+ }
832
+ // An activation that changed config `auth` makes the pin restart the gateway; then this resolves true once that
833
+ // restart has begun and the kit is ready again, false if it fails, is aborted or runs past RESTART_MS. No change: true.
834
+ restarted(authBefore, signal) {
835
+ if (this.o.spawnEngine === false || this.authSnapshot() === authBefore)
836
+ return Promise.resolve(true);
837
+ if (signal?.aborted)
838
+ return Promise.resolve(false);
839
+ return new Promise((resolve) => {
840
+ let began = this.engineRestarting();
841
+ const end = (ok) => {
842
+ clearTimeout(timer);
843
+ this.phaseListeners.delete(check);
844
+ signal?.removeEventListener('abort', abort);
845
+ resolve(ok);
846
+ };
847
+ const abort = () => end(false);
848
+ const check = () => {
849
+ if (this.engineRestarting())
850
+ began = true;
851
+ else if (began)
852
+ end(this.current.phase === 'ready');
853
+ };
854
+ const timer = setTimeout(() => end(false), RESTART_MS);
855
+ this.phaseListeners.add(check);
856
+ signal?.addEventListener('abort', abort, { once: true });
857
+ });
858
+ }
806
859
  closed(_why) {
807
860
  const engineFailed = _why === 'engine exited' && this.current.phase === 'failed' && this.current.why === 'exited';
808
861
  if (this.stopping || (this.current.phase !== 'ready' && !engineFailed))
@@ -839,9 +892,9 @@ export class OpenClawKit {
839
892
  delete this.browserProfiles[profile];
840
893
  }
841
894
  request() {
842
- if (!this.transport || this.current.phase !== 'ready')
843
- throw new Error('gateway not ready');
844
895
  const transport = this.transport;
896
+ if (!transport || this.current.phase !== 'ready')
897
+ return () => Promise.reject(new Error('gateway not ready'));
845
898
  return (method, params, options) => {
846
899
  const changesAuth = /^(models\.authLogout|openclaw\.setup\.(auth\.start|activate)|wizard\.(next|cancel)|config\.(apply|patch|set)|agents\.(create|update|delete)|secrets\.(reload|store\.(set|delete))|byokit\.keys)$/.test(method);
847
900
  if (!changesAuth || (method === 'byokit.keys' && params?.action === 'ready'))
@@ -853,22 +906,12 @@ export class OpenClawKit {
853
906
  call(method, params, o) {
854
907
  if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
855
908
  return Promise.reject(new Error('browser policy changes require guarded patchConfig'));
856
- try {
857
- return this.request()(method, params, o);
858
- }
859
- catch (error) {
860
- return Promise.reject(error);
861
- }
909
+ return this.request()(method, params, o);
862
910
  }
863
911
  callDynamic(method, params, o) {
864
912
  if (operatorMethods.has(method) || nodeMethods.has(method))
865
913
  return Promise.reject(new Error(`use typed call for generated method: ${method}`));
866
- try {
867
- return this.request()(method, params, o);
868
- }
869
- catch (error) {
870
- return Promise.reject(error);
871
- }
914
+ return this.request()(method, params, o);
872
915
  }
873
916
  get hello() { return this.greeting; }
874
917
  onEvent(event, fn) {
@@ -908,7 +951,9 @@ export class OpenClawKit {
908
951
  // Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
909
952
  authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
910
953
  ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
911
- onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; } };
954
+ onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
955
+ authConfig: () => this.authSnapshot(),
956
+ restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
912
957
  }
913
958
  routes() {
914
959
  return routeTable();
@@ -920,8 +965,10 @@ export class OpenClawKit {
920
965
  providers(member) {
921
966
  return this.providerStatus(member).then((names) => names ?? []);
922
967
  }
968
+ /** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
923
969
  signedIn(member, provider) {
924
- return this.providers(member).then((names) => names.includes(provider)).catch(() => false);
970
+ return this.providers(member).then(async (names) => names.includes(provider)
971
+ || providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider).catch(() => false);
925
972
  }
926
973
  signIn(member, o, on) {
927
974
  return startSignIn(this.signInCtx(), member, o, on);
@@ -929,14 +976,22 @@ export class OpenClawKit {
929
976
  signOut(member, provider) {
930
977
  return engineSignOut(this.signInCtx(), member, provider);
931
978
  }
932
- // accounts (5.15, O14 steps S5, S6, S9 and S10): the per-member index with listing, adding, defaults, rename, models
933
- // and room; `run({ account })` picks among them. Removing, `pick` and move arrive in later steps.
979
+ // The member an agent id or session key belongs to, through the account index (5.15, O14m). Reads the index file, so it
980
+ // needs no live gateway.
981
+ memberOf(ref) {
982
+ return memberOfAccount(this.engine.root, ref);
983
+ }
984
+ // accounts (5.15, O14 steps S5, S6 and S9-S12): the per-member index with listing, adding, defaults, rename, models
985
+ // and room; `run({ account })` and `pick` choose among them, held to the account a session is bound to, and `move`
986
+ // carries a conversation to another account. Removing arrives in a later step.
934
987
  accounts(member) {
935
988
  if (!this.accountsCtl)
936
989
  return Promise.reject(new Error('gateway not ready'));
937
990
  return this.accountsCtl.list(member);
938
991
  }
939
992
  models(member, id) {
993
+ if (typeof id !== 'string')
994
+ return this.keys.models(member, id);
940
995
  if (!this.accountsCtl)
941
996
  return Promise.reject(new Error('gateway not ready'));
942
997
  return this.accountsCtl.models(member, id);
@@ -946,11 +1001,27 @@ export class OpenClawKit {
946
1001
  return Promise.reject(new Error('gateway not ready'));
947
1002
  return this.accountsCtl.room(member, id, demand);
948
1003
  }
1004
+ /** What `run` would decide now with this selection and key, refusals included; decides nothing and runs nothing. */
1005
+ async pick(member, sel, o) {
1006
+ const accounts = this.accountsCtl;
1007
+ if (!accounts)
1008
+ throw new Error('gateway not ready');
1009
+ const binding = o?.sessionKey === undefined ? undefined : await accounts.bind(member, o.sessionKey);
1010
+ return (await accounts.pick(member, sel, [], binding)).pick;
1011
+ }
1012
+ /** Carry a conversation to another account: its whole transcript forks onto that account, then the old session goes. */
1013
+ move(ref, to) {
1014
+ if (!this.accountsCtl)
1015
+ return Promise.reject(new Error('gateway not ready'));
1016
+ return this.accountsCtl.move(ref.member, ref.sessionKey, to);
1017
+ }
949
1018
  /** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
950
1019
  addAccount(member, o, on) {
951
1020
  if (!this.accountsCtl)
952
1021
  return Promise.reject(new Error('gateway not ready'));
953
- return this.accountsCtl.add(member, o, on);
1022
+ return this.accountsCtl.add(member, o, on).then((added) => ({ ...added,
1023
+ done: added.done.then((r) => { if (r.view.state === 'done')
1024
+ this.syncCatalogs(); return r; }) }));
954
1025
  }
955
1026
  renameAccount(member, id, name) {
956
1027
  if (!this.accountsCtl)
@@ -971,6 +1042,37 @@ export class OpenClawKit {
971
1042
  addKey(agentId, o) {
972
1043
  return this.keys.add(agentId, o);
973
1044
  }
1045
+ async prepareKeyOwner(agentId) {
1046
+ this.syncCatalogs();
1047
+ await this.catalogs;
1048
+ if ((await authStatus(this.request(), agentId, true)).unavailable)
1049
+ throw new Error('Key owner unavailable.');
1050
+ }
1051
+ // Live provider catalogs (5.6): registers each live model with its api from the provider's catalog data, so
1052
+ // embedded runs resolve it. Serialized and in the background; a failure is logged and keeps the registered list.
1053
+ syncCatalogs() {
1054
+ for (const provider of LIVE_CATALOGS) {
1055
+ if (this.o.config?.models?.providers?.[provider]?.models !== undefined)
1056
+ continue;
1057
+ this.catalogs = this.catalogs.then(async () => {
1058
+ const answer = await this.request()('byokit.catalog', { provider });
1059
+ if (!answer.ok) {
1060
+ if (answer.reason !== 'no-key')
1061
+ this.o.log?.(`catalog sync ${provider}: ${answer.reason ?? 'failed'}`);
1062
+ return;
1063
+ }
1064
+ const { config } = await this.request()('config.get');
1065
+ const registered = config?.models?.providers?.[provider]?.models ?? [];
1066
+ const byId = new Map(registered.map((m) => [m.id, m]));
1067
+ for (const m of answer.models ?? [])
1068
+ byId.set(m.id, m);
1069
+ const models = [...byId.values()];
1070
+ if (!isDeepStrictEqual(models, registered))
1071
+ await this.patchConfig({ models: { providers: { [provider]: { models } } } });
1072
+ }).catch(() => { if (this.current.phase === 'ready')
1073
+ this.o.log?.(`catalog sync ${provider}: failed`); });
1074
+ }
1075
+ }
974
1076
  migrateRetainedLogin(member, source) {
975
1077
  return migrateLogin({ root: this.engine.root, prepare: () => this.prepare(), doctor: () => this.engine.doctor(120_000),
976
1078
  seal: this.o.authSeal, log: this.o.log, withStore: (task) => this.engine.withAuthStore(task) }, member, source);
@@ -1027,10 +1129,11 @@ export class OpenClawKit {
1027
1129
  const selected = await this.keys.ready(spec.member);
1028
1130
  if (!selected)
1029
1131
  return { ok: false, kind: 'signed-out', message: 'Add an API key to use this option.' };
1030
- if (spec.model !== undefined && spec.model !== selected.model)
1031
- return { ok: false, kind: 'plan', message: 'Choose the model saved with this key.' };
1132
+ // A key pays for its provider's models (5.8); the engine refuses one its catalog does not list.
1133
+ if (spec.model !== undefined && providerOf(spec.model) !== providerOf(selected.model))
1134
+ return { ok: false, kind: 'plan', message: 'Choose a model of the provider saved with this key.' };
1032
1135
  // RunRef.member stays the person for app gates; the engine receives the isolated agent and history.
1033
- return this.runs().run({ ...spec, model: selected.model }, on, selected.agentId, output);
1136
+ return this.runs().run({ ...spec, model: spec.model ?? selected.model }, on, selected.agentId, output);
1034
1137
  });
1035
1138
  return this.runs().run(spec, on, undefined, output);
1036
1139
  }
package/dist/link.js CHANGED
@@ -24,29 +24,32 @@ const isRecord = (value) => typeof value === 'object' && value !== null && !Arra
24
24
  const browserTag = (event) => event === 'byokit.browser';
25
25
  const browserEvent = (event, payload) => event === 'byokit.browser' && isRecord(payload) && typeof payload.member === 'string'
26
26
  && (payload.kind === 'state' || payload.kind === 'signin');
27
- const memberKey = (member, sessionKey) => sessionKey.startsWith(`agent:${member}:`);
28
- function memberSessions(result, member) {
27
+ function memberSessions(result, owns) {
29
28
  const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.sessions) ? result.sessions : [];
30
- return rows.filter((row) => isRecord(row) && typeof row.sessionKey === 'string' && memberKey(member, row.sessionKey));
29
+ return rows.filter((row) => isRecord(row) && typeof row.sessionKey === 'string' && owns(row.sessionKey));
31
30
  }
32
31
  /** `oc.run`'s run options, type-checked; the kit's own checks (account, tool names) still apply. */
33
32
  function runOptions(args) {
34
- const { message, model, auth, system, images, thinking, tools, schema } = args;
33
+ const { message, model, account, needs, auth, system, images, thinking, tools, schema } = args;
35
34
  const ok = typeof message === 'string'
36
35
  && (model === undefined || typeof model === 'string')
36
+ && (account === undefined || typeof account === 'string')
37
+ && (needs === undefined || (Array.isArray(needs) && needs.every((n) => typeof n === 'string')))
37
38
  && (auth === undefined || auth === 'apiKey')
38
39
  && (system === undefined || typeof system === 'string')
39
40
  && (images === undefined || (Array.isArray(images) && images.every((i) => isRecord(i) && typeof i.data === 'string' && typeof i.mimeType === 'string')))
40
41
  && (thinking === undefined || (typeof thinking === 'string' && THINKING.has(thinking)))
41
42
  && (tools === undefined || (Array.isArray(tools) && tools.every((t) => typeof t === 'string')));
42
43
  if (!ok)
43
- throw new Error('oc.run needs { message, sessionKey?, model?, auth?, system?, images?, thinking?, tools?, schema? }');
44
+ throw new Error('oc.run needs { message, sessionKey?, model?, account?, needs?, auth?, system?, images?, thinking?, tools?, schema? }');
44
45
  if (schema !== undefined)
45
46
  outputSchema(schema);
46
47
  return {
47
48
  message: message,
48
49
  ...(schema === undefined ? {} : { schema: schema }),
49
50
  ...(model === undefined ? {} : { model: model }),
51
+ ...(account === undefined ? {} : { account: account }),
52
+ ...(needs === undefined ? {} : { needs: [...needs] }),
50
53
  ...(auth === undefined ? {} : { auth: 'apiKey' }),
51
54
  ...(system === undefined ? {} : { system: system }),
52
55
  ...(images === undefined ? {} : { images: images
@@ -74,6 +77,7 @@ function browserInput(value) {
74
77
  }
75
78
  export function openclawLink(kit, o) {
76
79
  const passThrough = o.passThrough ?? (() => false);
80
+ const memberKey = (member, sessionKey) => kit.memberOf(sessionKey) === member;
77
81
  // The host lifecycle extension is supplied by the browser host. Without it control fails closed.
78
82
  const browser = () => {
79
83
  if (!kit.browser)
@@ -120,10 +124,14 @@ export function openclawLink(kit, o) {
120
124
  if (grant.expires !== undefined) {
121
125
  const timer = setTimeout(() => {
122
126
  const current = seen.get(grant.id);
123
- if (current?.expires !== undefined && current.expires > Date.now())
124
- memberOf(current);
125
- else
126
- void revoke(grant.id).catch(() => { });
127
+ if (current?.expires !== undefined && current.expires > Date.now()) {
128
+ try {
129
+ memberOf(current);
130
+ return;
131
+ }
132
+ catch { /* refused at the boundary: revoke below, never throw from a timer */ }
133
+ }
134
+ void revoke(grant.id).catch(() => { });
127
135
  }, Math.min(grant.expires - Date.now(), 2 ** 31 - 1));
128
136
  timer.unref();
129
137
  expiry.set(grant.id, timer);
@@ -355,7 +363,7 @@ export function openclawLink(kit, o) {
355
363
  }
356
364
  case 'oc.sessions': {
357
365
  const result = await kit.call('sessions.list', {});
358
- return memberSessions(result, member);
366
+ return memberSessions(result, (key) => memberKey(member, key));
359
367
  }
360
368
  case 'oc.steer': {
361
369
  if (typeof args.sessionKey !== 'string' || typeof args.text !== 'string' || (args.auth !== undefined && args.auth !== 'apiKey'))
@@ -584,9 +592,11 @@ export function openclawLink(kit, o) {
584
592
  void s.write(`${JSON.stringify({ event: 'byokit.browser', payload: { member, kind: record.kind } })}\n`).catch(() => { });
585
593
  return;
586
594
  }
595
+ // Attribution (5.15): an `<member>--<hex6>` account agent's events are the member's.
587
596
  const agent = record?.agentId;
588
597
  const key = record?.sessionKey;
589
- if (agent !== member && !(typeof key === 'string' && memberKey(member, key)))
598
+ if (!(typeof agent === 'string' && kit.memberOf(agent) === member)
599
+ && !(typeof key === 'string' && memberKey(member, key)))
590
600
  return;
591
601
  void s.write(`${JSON.stringify({ event, payload })}\n`).catch(() => { });
592
602
  });
package/dist/routes.d.ts CHANGED
@@ -17,5 +17,6 @@ export type RouteView = Route & {
17
17
  export declare function routes(facts?: RouteFacts): RouteView[];
18
18
  /** Retain explicit legacy selectors alongside corrected discovery metadata. A native CLI selector still
19
19
  * reaches engine detection (which checks its own login); it does not make an unavailable route default.
20
+ * An offered plan key is served whichever way was asked: it is always the pasted key.
20
21
  * API billing is never picked as a fallback. */
21
22
  export declare function routeFor(provider: string, via: 'browser' | 'code'): Route | undefined;
package/dist/routes.js CHANGED
@@ -41,9 +41,11 @@ export function routes(facts = {}) {
41
41
  }
42
42
  /** Retain explicit legacy selectors alongside corrected discovery metadata. A native CLI selector still
43
43
  * reaches engine detection (which checks its own login); it does not make an unavailable route default.
44
+ * An offered plan key is served whichever way was asked: it is always the pasted key.
44
45
  * API billing is never picked as a fallback. */
45
46
  export function routeFor(provider, via) {
46
- return table.find(route => route.billing === 'subscription' && route.offerPolicy === 'default'
47
- && ((route.offer && route.provider === provider && route.via === via)
48
- || (route.legacy?.provider === provider && route.legacy.via === via)));
47
+ const served = table.filter(route => route.billing === 'subscription' && route.offerPolicy === 'default');
48
+ return served.find(route => (route.offer && route.provider === provider && route.via === via)
49
+ || (route.legacy?.provider === provider && route.legacy.via === via))
50
+ ?? served.find(route => route.offer && route.provider === provider && route.keyEntry);
49
51
  }