@vymalo/opencode-lightbridge 0.16.1 → 0.17.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/README.md CHANGED
@@ -2,21 +2,25 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@vymalo/opencode-lightbridge?label=npm&color=CB3837&logo=npm)](https://www.npmjs.com/package/@vymalo/opencode-lightbridge)
4
4
 
5
- **One login, every egress.** The umbrella OpenCode plugin for [ADR-0012](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0012-single-auth-across-gateway-and-otel.md):
6
- a single shared `TokenRuntime` drives **both** the LLM gateway bearer and the OTEL export
7
- credential, because both validate the same `lightbridge-authz` issuer + `aud=lightbridge-api-key`.
8
- Log in once as yourself; every egress that needs a project-scoped credential rides the same token.
5
+ **The all-in-one plugin.** One credential drives everything: provider registration + model
6
+ discovery ([`register`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0017-lightbridge-all-in-one.md) —
7
+ "everything `@vymalo/opencode-oauth2` does"), the LLM gateway bearer (`gateway`), and the OTEL
8
+ export credential (`otel`) — see [ADR-0012](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0012-single-auth-across-gateway-and-otel.md)
9
+ and [ADR-0017](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0017-lightbridge-all-in-one.md).
10
+ Log in once as yourself; every module that needs a credential rides the same login — and since
11
+ ADR-0017, that login is shared with `@vymalo/opencode-oauth2` too, when both target the same IdP.
9
12
 
10
13
  Part of the [OpenCode Toolbelt](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit).
11
14
 
12
- ## Why this instead of `opencode-repo-auth` + `opencode-otel`
15
+ ## Why this instead of `opencode-oauth2` + `opencode-repo-auth` + `opencode-otel`
13
16
 
14
- Running both standalone plugins works, but each holds its **own** credential in its **own** cache
15
- namespace — two logins, two token stores, no relationship between them. `@vymalo/opencode-lightbridge`
16
- composes `@vymalo/opencode-auth-core` (the OAuth/token-exchange primitive) and
17
- `@vymalo/opencode-core-otel` (the OTel engine) over **one** `TokenRuntime`, so the project-scoped
18
- token minted for the gateway is the exact same token OTEL presents to the collector. No forked
19
- engine logic — this package is a thin composition layer over its two sibling libraries.
17
+ Running the standalone plugins works, but each holds its **own** credential in its **own** cache
18
+ namespace — up to three logins, three token stores, no relationship between them.
19
+ `@vymalo/opencode-lightbridge` composes `@vymalo/opencode-provider-sync` (the model-sync engine,
20
+ for `register`), `@vymalo/opencode-auth-core` (the OAuth/token-exchange primitive), and
21
+ `@vymalo/opencode-core-otel` (the OTel engine) over **one shared credential**, so the same token
22
+ that registers your provider is the one the gateway injects and OTEL presents to the collector. No
23
+ forked engine logic — this package composes its sibling libraries.
20
24
 
21
25
  ## Install
22
26
 
@@ -37,11 +41,14 @@ npm install @vymalo/opencode-lightbridge
37
41
  "authFlow": "device_code"
38
42
  },
39
43
  "gateway": {
40
- "projectId": "proj-123",
41
- "providers": ["gateway"]
44
+ "providers": ["gateway"],
45
+ "exchange": false
42
46
  },
43
47
  "otel": {
44
48
  "endpoint": "http://localhost:4318"
49
+ },
50
+ "register": {
51
+ "baseURL": "https://gateway.example.com/v1"
45
52
  }
46
53
  }]
47
54
  ]
@@ -49,17 +56,30 @@ npm install @vymalo/opencode-lightbridge
49
56
  ```
50
57
 
51
58
  - **`auth`** (required) — the one IdP login, an `AuthServerConfigInput` (same shape as
52
- `opencode-oauth2`/`opencode-repo-auth`'s per-provider auth block).
53
- - **`gateway`** (optional) — the OpenCode provider ids to inject `Authorization: Bearer
54
- <project-token>` on, per request (plus an optional `projectId`).
59
+ `opencode-oauth2`/`opencode-repo-auth`'s per-provider auth block). `auth.id` also doubles as the
60
+ OpenCode provider id `register` uses and the shared cache identity (see below).
61
+ - **`register`** (optional, [ADR-0017](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0017-lightbridge-all-in-one.md)) —
62
+ register `auth`'s IdP as an OpenCode provider and keep its models in sync, via the same
63
+ `@vymalo/opencode-provider-sync` engine `opencode-oauth2` uses. Requires `register.baseURL`.
64
+ - **`gateway`** (optional) — the OpenCode provider ids to inject a bearer on, per request (plus an
65
+ optional `gateway.exchange`/`projectId`).
66
+ - **`gateway.exchange`** (optional, default `false`) — `false`: use the IdP access token directly.
67
+ `true`: RFC 8693-exchange it for a project-scoped token first (ADR-0012's original behaviour).
55
68
  - **`otel`** (optional) — the same `OtelPluginOptions` shape as `@vymalo/opencode-otel`
56
69
  (`endpoint`, `exporters`, `serviceName`, …), minus `tokenCommand`/`tokenHeader`/`tokenPrefix`: the
57
70
  shared runtime supersedes that seam entirely.
58
- - **`projectId`** (optional, top-level or under `gateway`) — **fully optional**: omit it and the
59
- exchange sends no `project_id`, so the backend mints a token for your **default project**. An
60
- explicit top-level `projectId` wins over `gateway.projectId`.
71
+ - **`projectId`** (optional, top-level or under `gateway`) — only meaningful when
72
+ `gateway.exchange: true`. **Fully optional**: omit it and the exchange sends no `project_id`, so
73
+ the backend mints a token for your **default project**. An explicit top-level `projectId` wins
74
+ over `gateway.projectId`.
75
+
76
+ Omitting `gateway`, `otel` and `register` is a valid, inert config — the plugin logs and no-ops.
61
77
 
62
- Omitting both `gateway` and `otel` is a valid, inert config — the plugin logs and no-ops.
78
+ **One login, shared with oauth2.** Configure the same `id`/`issuer`/`clientId` as an
79
+ `@vymalo/opencode-oauth2` server entry and logging in through either plugin makes the token
80
+ available to both — no second device-code/browser flow. An existing pre-ADR-0017 install's cached
81
+ login is migrated automatically on first use; no re-login is forced. See
82
+ [`docs/lightbridge.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/lightbridge.md#one-login-shared-cache-with-oauth2-adr-0017).
63
83
 
64
84
  ## Scope: gateway + OTEL, not MCP
65
85
 
@@ -73,12 +93,15 @@ for the full reasoning.
73
93
  ## Full reference
74
94
 
75
95
  - [`docs/lightbridge.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/lightbridge.md)
76
- — the one-credential design, full config reference, the three egresses.
96
+ — the all-in-one design, full config reference, migration notes.
77
97
  - [ADR-0012](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0012-single-auth-across-gateway-and-otel.md)
78
98
  — why one runtime, why MCP is excluded, the alternatives considered.
79
- - [`docs/repo-auth.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/repo-auth.md)
80
- and [`docs/otel.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/otel.md)
81
- — the two standalone plugins this package composes; useful for the config shapes it reuses.
99
+ - [ADR-0017](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0017-lightbridge-all-in-one.md)
100
+ — `register`, the shared cache with oauth2, and the exchange becoming opt-in.
101
+ - [`docs/repo-auth.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/repo-auth.md),
102
+ [`docs/otel.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/otel.md) and
103
+ [`docs/architecture.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/architecture.md)
104
+ (oauth2) — the standalone plugins this package composes; useful for the config shapes it reuses.
82
105
 
83
106
  ## License
84
107
 
package/dist/config.d.ts CHANGED
@@ -7,9 +7,49 @@ export interface LightbridgeGatewayOptions {
7
7
  /**
8
8
  * Optional project id for the shared RFC 8693 exchange (ADR-0012). When
9
9
  * omitted, the exchange sends no `project_id` and the backend mints a token
10
- * for the caller's **default project**.
10
+ * for the caller's **default project**. Only meaningful when `exchange` is
11
+ * `true` — ignored otherwise (there is no exchange call to attach it to).
11
12
  */
12
13
  projectId?: string;
14
+ /**
15
+ * Whether to perform the RFC 8693 token exchange (ADR-0012) before using
16
+ * the result as the gateway bearer. Defaults to `false` (ADR-0017 amends
17
+ * ADR-0012): most IdP setups hand out an access token that is already
18
+ * scoped for the gateway, and an unconditional exchange fails outright for
19
+ * a client that has no `token-exchange` grant registered (`unauthorized_client`).
20
+ *
21
+ * - `false` (default): the IdP access token from `auth` is used directly
22
+ * as `Authorization: Bearer <token>` — no second network call, no extra
23
+ * grant required.
24
+ * - `true`: today's ADR-0012 behaviour, unchanged — the human token is
25
+ * exchanged for a project-scoped token (optionally carrying `project_id`)
26
+ * and THAT is used as the bearer.
27
+ */
28
+ exchange?: boolean;
29
+ }
30
+ /**
31
+ * The `register` opt-in (ADR-0017): register `auth`'s IdP as an OpenCode
32
+ * provider and keep its model list in sync via the SAME
33
+ * `@vymalo/opencode-provider-sync` engine `@vymalo/opencode-oauth2` uses —
34
+ * "everything oauth2 does", now available from lightbridge too. Independent
35
+ * of `gateway` (header injection) and `otel`: a config can register a
36
+ * provider without ever using it as the gateway bearer target, though the
37
+ * common case sets `gateway.providers` to the same id.
38
+ */
39
+ export interface LightbridgeRegisterOptions {
40
+ /** Base URL of the OpenAI-compatible (or Responses) inference endpoint. */
41
+ baseURL: string;
42
+ /** Display name for the registered OpenCode provider. Defaults to `auth.id`. */
43
+ name?: string;
44
+ /** Raw model id -> display name overrides, applied at discovery time. */
45
+ nameOverrides?: Record<string, string>;
46
+ /** Minutes between model-discovery syncs. Defaults to 60 (matches oauth2). */
47
+ syncIntervalMinutes?: number;
48
+ /**
49
+ * Route inference through the OpenAI Responses API instead of Chat
50
+ * Completions — see `@vymalo/opencode-provider-sync`'s `resolveProviderNpm`.
51
+ */
52
+ responseApi?: boolean;
13
53
  }
14
54
  /**
15
55
  * Plugin options as written under the `opencode-lightbridge` entry of
@@ -26,9 +66,12 @@ export interface LightbridgeOptions {
26
66
  * Optional project id for the shared project-scoped token exchange. Fully
27
67
  * optional: when omitted, the exchange sends no `project_id` and the backend
28
68
  * mints a token for the caller's **default project**. An explicit `projectId`
29
- * here wins over `gateway.projectId` when both are set.
69
+ * here wins over `gateway.projectId` when both are set. Only meaningful
70
+ * when `gateway.exchange` is `true`.
30
71
  */
31
72
  projectId?: string;
73
+ /** NEW (ADR-0017): register `auth`'s IdP as an OpenCode provider + sync its models. */
74
+ register?: LightbridgeRegisterOptions;
32
75
  }
33
76
  /** `LightbridgeOptions` after parsing: `auth` fully validated + defaulted. */
34
77
  export interface ParsedLightbridgeOptions {
@@ -37,6 +80,7 @@ export interface ParsedLightbridgeOptions {
37
80
  otel?: OtelPluginOptions;
38
81
  /** Resolved project id (`projectId` ?? `gateway.projectId`), if either was set. */
39
82
  projectId?: string;
83
+ register?: LightbridgeRegisterOptions;
40
84
  }
41
85
  /**
42
86
  * Parse + validate the plugin's `pluginOptions` (2nd factory arg). `auth` is
@@ -49,7 +93,26 @@ export interface ParsedLightbridgeOptions {
49
93
  export declare function parseLightbridgeOptions(raw: unknown): ParsedLightbridgeOptions;
50
94
  /**
51
95
  * Whether the parsed config has a module (gateway or otel) that needs the
52
- * shared token — project-scoped when `projectId` is set, else a default-project
53
- * token. Determines whether the one shared runtime is built.
96
+ * shared token — project-scoped when `projectId` is set (and `gateway.exchange`
97
+ * is `true`), else the raw IdP token. Determines whether the one shared
98
+ * runtime is built. `register` doesn't gate this: it drives a separate
99
+ * `ProviderModelSyncEngine`, not `LightbridgeRuntime`.
54
100
  */
55
101
  export declare function needsProjectToken(options: ParsedLightbridgeOptions): boolean;
102
+ export interface OpenCodeConfigLike {
103
+ provider?: unknown;
104
+ pluginConfig?: unknown;
105
+ }
106
+ /**
107
+ * Provider ids managed by `@vymalo/opencode-oauth2` via its
108
+ * `pluginConfig.oauth2ModelSync.servers` channel.
109
+ */
110
+ export declare function oauth2ManagedProviderIds(config: OpenCodeConfigLike): Set<string>;
111
+ /**
112
+ * Whether `providerId` is (also) managed by `@vymalo/opencode-oauth2` —
113
+ * either via that provider's own `options.oauth2` / `options.oauth2ModelSync`
114
+ * block, or via the `pluginConfig.oauth2ModelSync.servers` channel. Used by
115
+ * lightbridge's `register` module to skip registering/owning a provider
116
+ * oauth2 already claims (logged at `debug` — see `opencode.ts`).
117
+ */
118
+ export declare function hasOAuth2Conflict(config: OpenCodeConfigLike, providerId: string): boolean;
package/dist/config.js CHANGED
@@ -14,6 +14,15 @@ function asProviderList(value, path) {
14
14
  }
15
15
  return value.map((entry, index) => asNonEmptyString(entry, `${path}[${index}]`));
16
16
  }
17
+ function asBoolean(value, path) {
18
+ if (value === undefined || value === null) {
19
+ return undefined;
20
+ }
21
+ if (typeof value !== "boolean") {
22
+ throw new Error(`${path} must be a boolean (received ${JSON.stringify(value)})`);
23
+ }
24
+ return value;
25
+ }
17
26
  function parseGateway(raw, path) {
18
27
  if (raw === undefined || raw === null) {
19
28
  return undefined;
@@ -22,11 +31,44 @@ function parseGateway(raw, path) {
22
31
  throw new Error(`${path} must be an object with \`providers\` (and an optional \`projectId\`)`);
23
32
  }
24
33
  const projectId = raw.projectId === undefined || raw.projectId === null ? undefined : asNonEmptyString(raw.projectId, `${path}.projectId`);
34
+ // Opt-in (ADR-0017, amends ADR-0012): defaults to `false` — see
35
+ // `LightbridgeGatewayOptions.exchange`'s doc comment for the rationale.
36
+ const exchange = asBoolean(raw.exchange, `${path}.exchange`) ?? false;
25
37
  return {
26
38
  providers: asProviderList(raw.providers, `${path}.providers`),
39
+ exchange,
27
40
  ...projectId ? { projectId } : {}
28
41
  };
29
42
  }
43
+ function asStringMap(value, path) {
44
+ if (value === undefined || value === null) {
45
+ return undefined;
46
+ }
47
+ if (!isRecord(value)) {
48
+ throw new Error(`${path} must be an object mapping model id -> display name`);
49
+ }
50
+ const normalized = {};
51
+ for (const [key, entry] of Object.entries(value)) {
52
+ normalized[key] = asNonEmptyString(entry, `${path}.${key}`);
53
+ }
54
+ return normalized;
55
+ }
56
+ function parseRegister(raw, path) {
57
+ if (raw === undefined || raw === null) {
58
+ return undefined;
59
+ }
60
+ if (!isRecord(raw)) {
61
+ throw new Error(`${path} must be an object with at least \`baseURL\``);
62
+ }
63
+ const syncIntervalMinutes = typeof raw.syncIntervalMinutes === "number" && Number.isFinite(raw.syncIntervalMinutes) && raw.syncIntervalMinutes > 0 ? raw.syncIntervalMinutes : undefined;
64
+ return {
65
+ baseURL: asNonEmptyString(raw.baseURL, `${path}.baseURL`),
66
+ name: raw.name === undefined || raw.name === null ? undefined : asNonEmptyString(raw.name, `${path}.name`),
67
+ nameOverrides: asStringMap(raw.nameOverrides, `${path}.nameOverrides`),
68
+ syncIntervalMinutes,
69
+ responseApi: asBoolean(raw.responseApi, `${path}.responseApi`)
70
+ };
71
+ }
30
72
  function parseOtel(raw, path) {
31
73
  if (raw === undefined || raw === null) {
32
74
  return undefined;
@@ -57,22 +99,72 @@ export function parseLightbridgeOptions(raw) {
57
99
  const auth = validateAuthConfig(raw.auth);
58
100
  const gateway = parseGateway(raw.gateway, "lightbridge.gateway");
59
101
  const otel = parseOtel(raw.otel, "lightbridge.otel");
102
+ const register = parseRegister(raw.register, "lightbridge.register");
60
103
  const explicitProjectId = typeof raw.projectId === "string" && raw.projectId.trim().length > 0 ? raw.projectId.trim() : undefined;
61
104
  const projectId = explicitProjectId ?? gateway?.projectId;
62
105
  return {
63
106
  auth,
64
107
  gateway,
65
108
  otel,
66
- projectId
109
+ projectId,
110
+ register
67
111
  };
68
112
  }
69
113
  /**
70
114
  * Whether the parsed config has a module (gateway or otel) that needs the
71
- * shared token — project-scoped when `projectId` is set, else a default-project
72
- * token. Determines whether the one shared runtime is built.
115
+ * shared token — project-scoped when `projectId` is set (and `gateway.exchange`
116
+ * is `true`), else the raw IdP token. Determines whether the one shared
117
+ * runtime is built. `register` doesn't gate this: it drives a separate
118
+ * `ProviderModelSyncEngine`, not `LightbridgeRuntime`.
73
119
  */
74
120
  export function needsProjectToken(options) {
75
121
  return Boolean(options.gateway) || Boolean(options.otel);
76
122
  }
123
+ // ---- oauth2 provider-id collision guard (ADR-0017 requirement 4) ----------
124
+ //
125
+ // If a developer configures BOTH `@vymalo/opencode-oauth2` and lightbridge's
126
+ // `register` block against the SAME OpenCode provider id, they must not both
127
+ // register/own it — mirrors `@vymalo/opencode-repo-auth`'s
128
+ // `hasOAuth2Conflict`/`oauth2ManagedProviderIds` (which defers to oauth2 the
129
+ // same way). Kept as a lightbridge-local copy rather than a shared import:
130
+ // each consumer's own config-key literals are intentionally NOT centralized
131
+ // (see `@vymalo/opencode-provider-sync`'s `opencode-helpers.ts` module doc).
132
+ const OAUTH2_OPTION_KEYS = ["oauth2", "oauth2ModelSync"];
133
+ /**
134
+ * Provider ids managed by `@vymalo/opencode-oauth2` via its
135
+ * `pluginConfig.oauth2ModelSync.servers` channel.
136
+ */
137
+ export function oauth2ManagedProviderIds(config) {
138
+ const ids = new Set();
139
+ const pluginConfig = isRecord(config.pluginConfig) ? config.pluginConfig : undefined;
140
+ const oauth2ModelSync = isRecord(pluginConfig?.oauth2ModelSync) ? pluginConfig?.oauth2ModelSync : undefined;
141
+ const servers = oauth2ModelSync?.servers;
142
+ if (Array.isArray(servers)) {
143
+ for (const raw of servers) {
144
+ const entry = isRecord(raw) ? raw : undefined;
145
+ const id = typeof entry?.id === "string" ? entry.id : undefined;
146
+ if (id) {
147
+ ids.add(id);
148
+ }
149
+ }
150
+ }
151
+ return ids;
152
+ }
153
+ /**
154
+ * Whether `providerId` is (also) managed by `@vymalo/opencode-oauth2` —
155
+ * either via that provider's own `options.oauth2` / `options.oauth2ModelSync`
156
+ * block, or via the `pluginConfig.oauth2ModelSync.servers` channel. Used by
157
+ * lightbridge's `register` module to skip registering/owning a provider
158
+ * oauth2 already claims (logged at `debug` — see `opencode.ts`).
159
+ */
160
+ export function hasOAuth2Conflict(config, providerId) {
161
+ const providers = isRecord(config.provider) ? config.provider : undefined;
162
+ const providerEntry = providers ? providers[providerId] : undefined;
163
+ const providerOptions = isRecord(providerEntry) && isRecord(providerEntry.options) ? providerEntry.options : undefined;
164
+ if (providerOptions && OAUTH2_OPTION_KEYS.some((key) => isRecord(providerOptions[key]))) {
165
+ return true;
166
+ }
167
+ return oauth2ManagedProviderIds(config).has(providerId);
168
+ }
77
169
 
78
170
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"mappings":"AAAA,SACE,0BAGK;AA4CP,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,iBAAiB,OAAgB,MAAsB;CAC9D,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,WAAW,GAAG;EAC1D,MAAM,IAAI,MAAM,GAAG,KAAK,4BAA4B;CACtD;CACA,OAAO,MAAM,KAAK;AACpB;AAEA,SAAS,eAAe,OAAgB,MAAwB;CAC9D,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAAG;EAC/C,MAAM,IAAI,MAAM,GAAG,KAAK,2CAA2C;CACrE;CACA,OAAO,MAAM,KAAK,OAAO,UAAU,iBAAiB,OAAO,GAAG,KAAK,GAAG,MAAM,EAAE,CAAC;AACjF;AAEA,SAAS,aAAa,KAAc,MAAqD;CACvF,IAAI,QAAQ,aAAa,QAAQ,MAAM;EACrC,OAAO;CACT;CACA,IAAI,CAAC,SAAS,GAAG,GAAG;EAClB,MAAM,IAAI,MAAM,GAAG,KAAK,sEAAsE;CAChG;CACA,MAAM,YACJ,IAAI,cAAc,aAAa,IAAI,cAAc,OAC7C,YACA,iBAAiB,IAAI,WAAW,GAAG,KAAK,WAAW;CACzD,OAAO;EACL,WAAW,eAAe,IAAI,WAAW,GAAG,KAAK,WAAW;EAC5D,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;CACnC;AACF;AAEA,SAAS,UAAU,KAAc,MAA6C;CAC5E,IAAI,QAAQ,aAAa,QAAQ,MAAM;EACrC,OAAO;CACT;CACA,IAAI,CAAC,SAAS,GAAG,GAAG;EAClB,MAAM,IAAI,MAAM,GAAG,KAAK,uCAAuC;CACjE;;;;CAIA,OAAO;AACT;;;;;;;;;AAUA,OAAO,SAAS,wBAAwB,KAAwC;CAC9E,IAAI,CAAC,SAAS,GAAG,GAAG;EAClB,MAAM,IAAI,MACR,sFACF;CACF;CACA,IAAI,CAAC,SAAS,IAAI,IAAI,GAAG;EACvB,MAAM,IAAI,MAAM,4EAA4E;CAC9F;CAEA,MAAM,OAAO,mBAAmB,IAAI,IAAwC;CAC5E,MAAM,UAAU,aAAa,IAAI,SAAS,qBAAqB;CAC/D,MAAM,OAAO,UAAU,IAAI,MAAM,kBAAkB;CAEnD,MAAM,oBACJ,OAAO,IAAI,cAAc,YAAY,IAAI,UAAU,KAAK,CAAC,CAAC,SAAS,IAC/D,IAAI,UAAU,KAAK,IACnB;CACN,MAAM,YAAY,qBAAqB,SAAS;CAEhD,OAAO;EAAE;EAAM;EAAS;EAAM;CAAU;AAC1C;;;;;;AAOA,OAAO,SAAS,kBAAkB,SAA4C;CAC5E,OAAO,QAAQ,QAAQ,OAAO,KAAK,QAAQ,QAAQ,IAAI;AACzD","names":[],"sources":["../src/config.ts"],"version":3,"file":"config.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/config.ts"],"version":3,"file":"config.js","sourceRoot":""}
package/dist/lib.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { createLightbridgePlugin, LightbridgePlugin, type LightbridgePluginFactoryOptions } from "./opencode.js";
2
- export { LightbridgeRuntime, LIGHTBRIDGE_IDENTITY, DEFAULT_CACHE_NAMESPACE, DEFAULT_PROJECT_KEY, lightbridgeCacheDir, type LightbridgeRuntimeFactory, type LightbridgeRuntimeLike, type LightbridgeRuntimeOptions } from "./plugin.js";
3
- export { parseLightbridgeOptions, needsProjectToken, type LightbridgeGatewayOptions, type LightbridgeOptions, type ParsedLightbridgeOptions } from "./config.js";
2
+ export { LightbridgeRuntime, LIGHTBRIDGE_IDENTITY, DEFAULT_CACHE_NAMESPACE, DEFAULT_PROJECT_KEY, ROOT_CACHE_NAMESPACE, ROOT_CACHE_SEGMENT, lightbridgeCacheDir, rootCacheDir, type LightbridgeRuntimeFactory, type LightbridgeRuntimeLike, type LightbridgeRuntimeOptions } from "./plugin.js";
3
+ export { hasOAuth2Conflict, needsProjectToken, oauth2ManagedProviderIds, parseLightbridgeOptions, type LightbridgeGatewayOptions, type LightbridgeOptions, type LightbridgeRegisterOptions, type OpenCodeConfigLike, type ParsedLightbridgeOptions } from "./config.js";
4
+ export { migrateRootTokenIfNeeded } from "./migration.js";
package/dist/lib.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createLightbridgePlugin, LightbridgePlugin } from "./opencode.js";
2
- export { LightbridgeRuntime, LIGHTBRIDGE_IDENTITY, DEFAULT_CACHE_NAMESPACE, DEFAULT_PROJECT_KEY, lightbridgeCacheDir } from "./plugin.js";
3
- export { parseLightbridgeOptions, needsProjectToken } from "./config.js";
2
+ export { LightbridgeRuntime, LIGHTBRIDGE_IDENTITY, DEFAULT_CACHE_NAMESPACE, DEFAULT_PROJECT_KEY, ROOT_CACHE_NAMESPACE, ROOT_CACHE_SEGMENT, lightbridgeCacheDir, rootCacheDir } from "./plugin.js";
3
+ export { hasOAuth2Conflict, needsProjectToken, oauth2ManagedProviderIds, parseLightbridgeOptions } from "./config.js";
4
+ export { migrateRootTokenIfNeeded } from "./migration.js";
4
5
 
5
6
  //# sourceMappingURL=lib.js.map
package/dist/lib.js.map CHANGED
@@ -1 +1 @@
1
- {"mappings":"AAAA,SACE,yBACA,yBAEK;AAEP,SACE,oBACA,sBACA,yBACA,qBACA,2BAIK;AAEP,SACE,yBACA,yBAIK","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
1
+ {"mappings":"AAAA,SACE,yBACA,yBAEK;AAEP,SACE,oBACA,sBACA,yBACA,qBACA,sBACA,oBACA,qBACA,oBAIK;AAEP,SACE,mBACA,mBACA,0BACA,+BAMK;AAEP,SAAS,gCAAgC","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
@@ -0,0 +1,31 @@
1
+ import { type Logger } from "@vymalo/opencode-auth-core/lib";
2
+ /**
3
+ * One-time migration read-path (ADR-0017, mandatory — not optional polish).
4
+ *
5
+ * v0.17 moves lightbridge's human root token from its own bespoke file
6
+ * (`<cacheRoot>/opencode-lightbridge/lightbridge.json`, a bare `TokenSet`) to
7
+ * the SAME fused `CachedServerState` file `@vymalo/opencode-oauth2` uses
8
+ * (`<cacheRoot>/<sharedSegment>/<sharedNamespace>/<serverId>.json`) so the two
9
+ * plugins can share one login (ADR-0017 §"one login, shared cache"). An
10
+ * install that already has a valid old-format token must NOT be forced
11
+ * through a fresh device-code/browser login just because the file moved.
12
+ *
13
+ * Behaviour:
14
+ * - If the NEW location already has a persisted state (with or without a
15
+ * token), this is a no-op — never clobbers a fresher state (e.g. one
16
+ * oauth2 has already been writing to, with real discovered models).
17
+ * - Else, read the OLD location. If it holds a structurally usable token
18
+ * (`accessToken` + `tokenType` present — possibly expired; an expired
19
+ * token with a refresh token is still worth adopting, since `TokenRuntime`
20
+ * will refresh it transparently), write it into the NEW location as a
21
+ * fresh `CachedServerState` (empty `models`/`rawModels` — nothing to carry
22
+ * over, since the old shape never tracked models).
23
+ * - Else (no old file, or it's empty/malformed) this is a no-op; the caller
24
+ * proceeds to a normal login.
25
+ *
26
+ * The old file is left in place (non-destructive) — this function only ever
27
+ * ADDS the new file, never deletes/mutates the old one. Safe to call more
28
+ * than once: the "new location already has a state" check makes every call
29
+ * after the first a cheap no-op.
30
+ */
31
+ export declare function migrateRootTokenIfNeeded(cacheRoot: string, sharedSegment: string, sharedNamespace: string, serverId: string, logger: Logger): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import { join } from "node:path";
2
+ import { FileCacheStore as LegacyFileCacheStore } from "@vymalo/opencode-auth-core/lib";
3
+ import { FileCacheStore as SharedFileCacheStore } from "@vymalo/opencode-provider-sync/lib";
4
+ /**
5
+ * The pre-ADR-0017 on-disk location of lightbridge's human root token:
6
+ * `<cacheRoot>/opencode-lightbridge/lightbridge.json`, a bare `TokenSet`
7
+ * (see the OLD `LIGHTBRIDGE_IDENTITY`/`DEFAULT_CACHE_NAMESPACE` in `plugin.ts`,
8
+ * which are still used verbatim for the exchanged project-token store — only
9
+ * the ROOT token relocated).
10
+ */
11
+ const OLD_ROOT_CACHE_NAMESPACE = "opencode-lightbridge";
12
+ const OLD_ROOT_KEY = "lightbridge";
13
+ function hasUsableTokenShape(value) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
15
+ return false;
16
+ }
17
+ const candidate = value;
18
+ return typeof candidate.accessToken === "string" && candidate.accessToken.length > 0 && typeof candidate.tokenType === "string" && candidate.tokenType.length > 0;
19
+ }
20
+ /**
21
+ * One-time migration read-path (ADR-0017, mandatory — not optional polish).
22
+ *
23
+ * v0.17 moves lightbridge's human root token from its own bespoke file
24
+ * (`<cacheRoot>/opencode-lightbridge/lightbridge.json`, a bare `TokenSet`) to
25
+ * the SAME fused `CachedServerState` file `@vymalo/opencode-oauth2` uses
26
+ * (`<cacheRoot>/<sharedSegment>/<sharedNamespace>/<serverId>.json`) so the two
27
+ * plugins can share one login (ADR-0017 §"one login, shared cache"). An
28
+ * install that already has a valid old-format token must NOT be forced
29
+ * through a fresh device-code/browser login just because the file moved.
30
+ *
31
+ * Behaviour:
32
+ * - If the NEW location already has a persisted state (with or without a
33
+ * token), this is a no-op — never clobbers a fresher state (e.g. one
34
+ * oauth2 has already been writing to, with real discovered models).
35
+ * - Else, read the OLD location. If it holds a structurally usable token
36
+ * (`accessToken` + `tokenType` present — possibly expired; an expired
37
+ * token with a refresh token is still worth adopting, since `TokenRuntime`
38
+ * will refresh it transparently), write it into the NEW location as a
39
+ * fresh `CachedServerState` (empty `models`/`rawModels` — nothing to carry
40
+ * over, since the old shape never tracked models).
41
+ * - Else (no old file, or it's empty/malformed) this is a no-op; the caller
42
+ * proceeds to a normal login.
43
+ *
44
+ * The old file is left in place (non-destructive) — this function only ever
45
+ * ADDS the new file, never deletes/mutates the old one. Safe to call more
46
+ * than once: the "new location already has a state" check makes every call
47
+ * after the first a cheap no-op.
48
+ */
49
+ export async function migrateRootTokenIfNeeded(cacheRoot, sharedSegment, sharedNamespace, serverId, logger) {
50
+ const sharedStore = new SharedFileCacheStore(join(cacheRoot, sharedSegment, sharedNamespace), logger);
51
+ let alreadyMigrated;
52
+ try {
53
+ alreadyMigrated = await sharedStore.loadServerState(serverId);
54
+ } catch (error) {
55
+ logger.debug("lightbridge_root_migration_check_failed", { error: error instanceof Error ? error.message : String(error) });
56
+ return;
57
+ }
58
+ if (alreadyMigrated) {
59
+ // New location already has SOME state (with or without a token) — either
60
+ // a previous migration ran, or oauth2/register already populated it.
61
+ // Never overwrite it from the old file.
62
+ return;
63
+ }
64
+ const legacyStore = new LegacyFileCacheStore(join(cacheRoot, OLD_ROOT_CACHE_NAMESPACE), logger);
65
+ let legacyToken;
66
+ try {
67
+ legacyToken = await legacyStore.load(OLD_ROOT_KEY);
68
+ } catch (error) {
69
+ logger.debug("lightbridge_root_migration_legacy_read_failed", { error: error instanceof Error ? error.message : String(error) });
70
+ return;
71
+ }
72
+ if (!hasUsableTokenShape(legacyToken)) {
73
+ logger.trace("lightbridge_root_migration_nothing_to_adopt", { serverId });
74
+ return;
75
+ }
76
+ const migrated = {
77
+ serverId,
78
+ updatedAt: Date.now(),
79
+ models: [],
80
+ rawModels: [],
81
+ token: legacyToken
82
+ };
83
+ await sharedStore.ensureReady();
84
+ await sharedStore.saveServerState(migrated);
85
+ logger.info("lightbridge_root_migration_adopted", {
86
+ serverId,
87
+ hadRefreshToken: Boolean(legacyToken.refreshToken)
88
+ });
89
+ }
90
+
91
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"mappings":"AAAA,SAAS,YAAY;AAErB,SACE,kBAAkB,4BAGb;AACP,SACE,kBAAkB,4BAEb;;;;;;;;AASP,MAAM,2BAA2B;AACjC,MAAM,eAAe;AAErB,SAAS,oBAAoB,OAAmC;CAC9D,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;EAC/D,OAAO;CACT;CACA,MAAM,YAAY;CAClB,OACE,OAAO,UAAU,gBAAgB,YACjC,UAAU,YAAY,SAAS,KAC/B,OAAO,UAAU,cAAc,YAC/B,UAAU,UAAU,SAAS;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,OAAO,eAAe,yBACpB,WACA,eACA,iBACA,UACA,QACe;CACf,MAAM,cAAc,IAAI,qBACtB,KAAK,WAAW,eAAe,eAAe,GAC9C,MACF;CAEA,IAAI;CACJ,IAAI;EACF,kBAAkB,MAAM,YAAY,gBAAgB,QAAQ;CAC9D,SAAS,OAAO;EACd,OAAO,MAAM,2CAA2C,EACtD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;EACD;CACF;CACA,IAAI,iBAAiB;;;;EAInB;CACF;CAEA,MAAM,cAAc,IAAI,qBAAqB,KAAK,WAAW,wBAAwB,GAAG,MAAM;CAC9F,IAAI;CACJ,IAAI;EACF,cAAc,MAAM,YAAY,KAAe,YAAY;CAC7D,SAAS,OAAO;EACd,OAAO,MAAM,iDAAiD,EAC5D,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;EACD;CACF;CAEA,IAAI,CAAC,oBAAoB,WAAW,GAAG;EACrC,OAAO,MAAM,+CAA+C,EAAE,SAAS,CAAC;EACxE;CACF;CAEA,MAAM,WAA8B;EAClC;EACA,WAAW,KAAK,IAAI;EACpB,QAAQ,CAAC;EACT,WAAW,CAAC;EACZ,OAAO;CACT;CAEA,MAAM,YAAY,YAAY;CAC9B,MAAM,YAAY,gBAAgB,QAAQ;CAC1C,OAAO,KAAK,sCAAsC;EAChD;EACA,iBAAiB,QAAQ,YAAY,YAAY;CACnD,CAAC;AACH","names":[],"sources":["../src/migration.ts"],"version":3,"file":"migration.js","sourceRoot":""}
package/dist/opencode.js CHANGED
@@ -1,12 +1,23 @@
1
1
  import { hostname } from "node:os";
2
+ import { join } from "node:path";
2
3
  import { createJsonConsoleLogger, DEFAULT_LOG_LEVEL, LOG_LEVEL_PRIORITY } from "@vymalo/opencode-auth-core/lib";
3
4
  import { buildResource, createProviders, deferredAttribute, describeError, fromOpenCodeLogLevel, installTracePropagation, readVcsInfo, resolveOtelConfig, TelemetryRecorder } from "@vymalo/opencode-core-otel";
4
- import { needsProjectToken, parseLightbridgeOptions } from "./config.js";
5
- import { LightbridgeRuntime } from "./plugin.js";
5
+ import { applyResponsesApiOptions, mergeDiscoveredModels, ProviderModelSyncEngine, resolveProviderNpm } from "@vymalo/opencode-provider-sync/lib";
6
+ import { hasOAuth2Conflict, needsProjectToken, parseLightbridgeOptions } from "./config.js";
7
+ import { LightbridgeRuntime, ROOT_CACHE_NAMESPACE, ROOT_CACHE_SEGMENT } from "./plugin.js";
6
8
  const PLUGIN_SERVICE_NAME = "opencode-lightbridge-plugin";
7
9
  /**
8
10
  * Pipe plugin logs through OpenCode's `client.app.log`, JSON console as
9
11
  * fallback. Mirrors every other plugin in the suite.
12
+ *
13
+ * This plugin mutates the session (gateway bearer injection, OTEL export), so
14
+ * it might seem like a warn/error printed to the terminal would be actionable
15
+ * developer feedback. It is not: no plugin in this suite may write to the
16
+ * user's TUI (ADR-0014, superseding ADR-0013's narrower per-plugin scope).
17
+ * Every record still reaches `client.app.log` at its true level, so the
18
+ * diagnostic is always persisted to OpenCode's own log — just not on the
19
+ * developer's screen. Set `VYMALO_PLUGIN_CONSOLE_LOG=1` (`consoleAll` below)
20
+ * to opt back into the console mirror for every level.
10
21
  */
11
22
  function createOpenCodeLogger(client, getMinLevel) {
12
23
  const fallback = createJsonConsoleLogger("debug");
@@ -15,7 +26,7 @@ function createOpenCodeLogger(client, getMinLevel) {
15
26
  if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[getMinLevel()]) {
16
27
  return;
17
28
  }
18
- if (consoleAll || level === "warn" || level === "error") {
29
+ if (consoleAll) {
19
30
  fallback[level](event, fields);
20
31
  }
21
32
  const hostLevel = level === "trace" ? "debug" : level;
@@ -72,8 +83,15 @@ function registerExitHandlers(providers, logger, deferred) {
72
83
  * async factory over the SHARED runtime, superseding otel's standalone
73
84
  * `tokenCommand` path entirely (ADR-0012 — one credential, not two seams).
74
85
  * Never throws — the OTLP exporters require that of every `headers()` call —
75
- * so an exchange failure degrades to an unauthenticated export, which the
76
- * collector then rejects (fail closed, same posture as the gateway header).
86
+ * so an exchange failure resolves `headers()` to `undefined` rather than `{}`.
87
+ * That distinction is load-bearing (ADR-0015): `{}` would still let the export
88
+ * fire with no `Authorization` header, which the collector then rejects at the
89
+ * network — the exact bug this fixes. `undefined` tells the shared gate in
90
+ * `@vymalo/opencode-core-otel` (`withFailureLogging`) to skip the export
91
+ * before any request goes out, so a logged-out session goes quiet instead of
92
+ * hammering the collector with 401s. Because `headers()` is called fresh on
93
+ * every export attempt, a later successful login resumes exporting on the
94
+ * next flush with nothing to restart.
77
95
  */
78
96
  function createRuntimeTokenSource(runtime) {
79
97
  return {
@@ -82,7 +100,7 @@ function createRuntimeTokenSource(runtime) {
82
100
  const token = await runtime.getProjectToken({ interactive: false });
83
101
  return { Authorization: `${token.tokenType || "Bearer"} ${token.accessToken}` };
84
102
  } catch {
85
- return {};
103
+ return undefined;
86
104
  }
87
105
  },
88
106
  // v1: no-op. The project token is short-lived and `getProjectToken`
@@ -114,6 +132,117 @@ function createGatewayChatHeaders(providers, runtime, logger) {
114
132
  };
115
133
  }
116
134
  /**
135
+ * Translate `auth` (already-validated `AuthServerConfig`) + `register` into
136
+ * the ONE `ProviderServerConfig` the shared `ProviderModelSyncEngine` needs
137
+ * (ADR-0017 requirement 1 — "everything oauth2 does"). `auth.id` is the
138
+ * provider id AND the shared cache identity (see `plugin.ts`'s
139
+ * `ROOT_CACHE_SEGMENT`/`ROOT_CACHE_NAMESPACE`).
140
+ */
141
+ function buildRegisterServerConfig(auth, register) {
142
+ return {
143
+ id: auth.id,
144
+ name: register.name ?? auth.id,
145
+ issuer: auth.issuer,
146
+ baseURL: register.baseURL,
147
+ clientId: auth.clientId,
148
+ clientSecret: auth.clientSecret,
149
+ scopes: auth.scopes,
150
+ syncIntervalMinutes: register.syncIntervalMinutes ?? 60,
151
+ nameOverrides: register.nameOverrides ?? {},
152
+ authorizationEndpoint: auth.authorizationEndpoint,
153
+ tokenEndpoint: auth.tokenEndpoint,
154
+ deviceAuthorizationEndpoint: auth.deviceAuthorizationEndpoint,
155
+ jwksUri: auth.jwksUri,
156
+ redirectPort: auth.redirectPort,
157
+ authFlow: auth.authFlow,
158
+ pkce: auth.pkce,
159
+ subjectTokenSource: auth.subjectTokenSource,
160
+ tokenExchangeAudience: auth.tokenExchangeAudience
161
+ };
162
+ }
163
+ /**
164
+ * The `register` module (ADR-0017 requirement 1): register `auth`'s IdP as an
165
+ * OpenCode provider and keep `config.provider[<auth.id>].models` in sync via
166
+ * the SAME `ProviderModelSyncEngine` `@vymalo/opencode-oauth2` uses — built
167
+ * ONCE (register's config is fixed at plugin-construction time, unlike
168
+ * oauth2's, which is re-derived from the host config on every `config` hook),
169
+ * then merged into the host config on every `config` hook call so a later
170
+ * OpenCode config pass still sees the discovered models.
171
+ *
172
+ * Guard (requirement 4a): if `auth.id` is ALSO managed by
173
+ * `@vymalo/opencode-oauth2` in this same host config, lightbridge must not
174
+ * also register/own it — skip entirely (never construct the engine, never
175
+ * touch `config.provider[id]`), logged at `debug` (ADR-0014: nothing reaches
176
+ * the terminal), and fail safe (the gateway/OTEL bearer still works via
177
+ * `LightbridgeRuntime`'s own lightweight, read/refresh-only participation in
178
+ * the same shared cache file). The double-scheduling guard (requirement 4b —
179
+ * two engine instances in one process ticking the same cache file) is
180
+ * enforced generically inside `ProviderModelSyncEngine.start()` itself.
181
+ */
182
+ async function applyRegisterModule(hostConfig, register, auth, state, logger, factoryOptions) {
183
+ const providerId = auth.id;
184
+ if (!state.engine && hasOAuth2Conflict(hostConfig, providerId)) {
185
+ if (!state.skippedForConflict) {
186
+ state.skippedForConflict = true;
187
+ logger.debug("lightbridge_register_skipped_oauth2_conflict", { providerId });
188
+ }
189
+ return;
190
+ }
191
+ if (!state.engine) {
192
+ state.engine = new ProviderModelSyncEngine({
193
+ servers: [buildRegisterServerConfig(auth, register)],
194
+ cacheNamespace: ROOT_CACHE_NAMESPACE
195
+ }, {
196
+ logger,
197
+ fetchImpl: factoryOptions.fetchImpl,
198
+ onAuthorizationUrl: factoryOptions.onAuthorizationUrl,
199
+ cacheDir: factoryOptions.cacheDir ? join(factoryOptions.cacheDir, ROOT_CACHE_SEGMENT, ROOT_CACHE_NAMESPACE) : undefined,
200
+ cacheNamespaceSegment: ROOT_CACHE_SEGMENT,
201
+ serviceLabel: "opencode-lightbridge"
202
+ });
203
+ }
204
+ if (!state.started) {
205
+ await state.engine.initialize();
206
+ await state.engine.start({ warmup: true });
207
+ state.started = true;
208
+ }
209
+ const providers = hostConfig.provider ??= {};
210
+ const providerConfig = providers[providerId] ??= {};
211
+ const providerOptions = providerConfig.options ?? {};
212
+ providerConfig.npm = resolveProviderNpm(register.responseApi);
213
+ providerConfig.name = providerConfig.name ?? register.name ?? providerId;
214
+ providerConfig.options = applyResponsesApiOptions({
215
+ ...providerOptions,
216
+ baseURL: register.baseURL
217
+ }, register.responseApi, providerId, logger);
218
+ const models = state.engine.getServerModels(providerId);
219
+ if (models.length > 0) {
220
+ mergeDiscoveredModels(providerConfig, models);
221
+ }
222
+ logger.trace("lightbridge_register_config_applied", {
223
+ providerId,
224
+ modelCount: models.length
225
+ });
226
+ }
227
+ /**
228
+ * `otel.tokenCommand`/`tokenHeader`/`tokenPrefix` are accepted by
229
+ * `parseLightbridgeOptions` (they are just `OtelPluginOptions` fields) but
230
+ * never take effect here: `buildOtelModule` always injects the shared
231
+ * runtime-backed `TokenSource` whenever a runtime exists, and that injected
232
+ * source unconditionally wins over `config.tokenCommand` in
233
+ * `createProviders` (see `providers.ts`). Silently accepting and ignoring a
234
+ * credential-helper config is exactly the kind of no-op ADR-0015 calls out, so
235
+ * this logs once at `debug` rather than staying quiet — `debug` because it's
236
+ * an informational no-op, not a warning-worthy misconfiguration, and no
237
+ * plugin in this suite prints to the terminal by default anyway (ADR-0014).
238
+ */
239
+ function warnIfTokenCommandConfigIgnored(otel, env, logger) {
240
+ const explicit = otel.tokenCommand !== undefined || otel.tokenHeader !== undefined || otel.tokenPrefix !== undefined || Boolean(env.OPENCODE_OTEL_TOKEN_COMMAND) || Boolean(env.OPENCODE_OTEL_TOKEN_HEADER) || Boolean(env.OPENCODE_OTEL_TOKEN_PREFIX);
241
+ if (explicit) {
242
+ logger.debug("lightbridge_otel_token_command_ignored", { reason: "otel.tokenCommand/tokenHeader/tokenPrefix are ignored — the shared lightbridge auth token always supersedes them" });
243
+ }
244
+ }
245
+ /**
117
246
  * Build the OTEL module: resource, providers (with the runtime-backed
118
247
  * `TokenSource` when a shared runtime is available), recorder and exit
119
248
  * handlers. Mirrors `@vymalo/opencode-otel`'s `createOtelPlugin` orchestration
@@ -235,9 +364,15 @@ export function createLightbridgePlugin(factoryOptions = {}) {
235
364
  fetchImpl: factoryOptions.fetchImpl,
236
365
  onAuthorizationUrl: factoryOptions.onAuthorizationUrl,
237
366
  cacheDir: factoryOptions.cacheDir,
238
- tokenExpirySkewMs: factoryOptions.tokenExpirySkewMs
367
+ tokenExpirySkewMs: factoryOptions.tokenExpirySkewMs,
368
+ exchange: parsed.gateway?.exchange ?? false
239
369
  }) : undefined;
240
370
  const hooks = {};
371
+ // ---- register module (ADR-0017 requirement 1) ----
372
+ const registerState = {
373
+ started: false,
374
+ skippedForConflict: false
375
+ };
241
376
  // ---- gateway module ----
242
377
  // `sharedRuntime` is always defined here: it is built whenever `gateway`
243
378
  // or `otel` is configured (`wantsProjectToken`).
@@ -249,6 +384,7 @@ export function createLightbridgePlugin(factoryOptions = {}) {
249
384
  if (parsed.otel) {
250
385
  const otelConfig = resolveOtelConfig(parsed.otel, factoryOptions.env ?? process.env);
251
386
  if (otelConfig.active) {
387
+ warnIfTokenCommandConfigIgnored(parsed.otel, factoryOptions.env ?? process.env, logger);
252
388
  otel = await buildOtelModule(input, otelConfig, logger, factoryOptions, sharedRuntime);
253
389
  registerOtelHooks(hooks, otel);
254
390
  } else {
@@ -260,6 +396,9 @@ export function createLightbridgePlugin(factoryOptions = {}) {
260
396
  }
261
397
  hooks.config = async (hostConfig) => {
262
398
  currentLogLevel = fromOpenCodeLogLevel(hostConfig.logLevel) ?? DEFAULT_LOG_LEVEL;
399
+ if (parsed.register) {
400
+ await applyRegisterModule(hostConfig, parsed.register, parsed.auth, registerState, logger, factoryOptions);
401
+ }
263
402
  if (otel?.config.propagateTraceContext) {
264
403
  const activeOtel = otel;
265
404
  const wrapped = installTracePropagation(hostConfig, {
@@ -271,7 +410,9 @@ export function createLightbridgePlugin(factoryOptions = {}) {
271
410
  };
272
411
  logger.info("lightbridge_plugin_ready", {
273
412
  gateway: Boolean(hooks["chat.headers"]),
413
+ exchange: parsed.gateway?.exchange ?? false,
274
414
  otel: Boolean(otel),
415
+ register: Boolean(parsed.register),
275
416
  projectId: parsed.projectId ?? (wantsProjectToken ? "(default)" : undefined)
276
417
  });
277
418
  return hooks;
@@ -1 +1 @@
1
- {"mappings":"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","names":[],"sources":["../src/opencode.ts"],"version":3,"file":"opencode.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/opencode.ts"],"version":3,"file":"opencode.js","sourceRoot":""}
package/dist/plugin.d.ts CHANGED
@@ -1,12 +1,28 @@
1
1
  import { type AuthServerConfig, type Logger, type TokenSet } from "@vymalo/opencode-auth-core/lib";
2
2
  /**
3
- * Identity for the umbrella's single `TokenRuntime` (ADR-0012). Constant
4
- * because v1 is single-IdP, same rationale as repo-auth's `HUMAN_IDENTITY`.
3
+ * Legacy identity for the exchanged project-token store (UNCHANGED by
4
+ * ADR-0017 — see `migration.ts`'s module doc for why only the ROOT token
5
+ * relocated). Kept as a constant, not `auth.id`, because the exchanged-token
6
+ * cache key has always been derived from this literal, independent of the
7
+ * configured IdP's `id`.
5
8
  */
6
9
  export declare const LIGHTBRIDGE_IDENTITY = "lightbridge";
7
- /** Own cache namespace — separate from oauth2/repo-auth/otel's stores. */
10
+ /** Own cache namespace for the exchanged-token store — separate from oauth2/repo-auth/otel's. */
8
11
  export declare const DEFAULT_CACHE_NAMESPACE = "opencode-lightbridge";
9
12
  /**
13
+ * ADR-0017: the human ROOT token now lives in the SAME cache file
14
+ * `@vymalo/opencode-oauth2` uses for a server of this id —
15
+ * `<cacheRoot>/opencode-oauth2/opencode-oauth2-model-sync/<id>.json`, a fused
16
+ * `CachedServerState` (token + discovered models). This is what lets a
17
+ * developer who configures the same IdP (`auth.id` == an oauth2 `servers[].id`,
18
+ * same issuer/clientId) log in ONCE and have both plugins read/write the same
19
+ * on-disk state. The literals are pinned here (not imported from
20
+ * `@vymalo/opencode-oauth2`, which lightbridge does not depend on) — see
21
+ * `docs/adr/0017-lightbridge-all-in-one.md` for the shared-contract rationale.
22
+ */
23
+ export declare const ROOT_CACHE_SEGMENT = "opencode-oauth2";
24
+ export declare const ROOT_CACHE_NAMESPACE = "opencode-oauth2-model-sync";
25
+ /**
10
26
  * Cache key for the default-project token — used when no `projectId` is
11
27
  * configured, so the exchange sends no `project_id` and the backend mints a
12
28
  * token for the caller's default project (ADR-0012). Distinct from any real
@@ -17,9 +33,16 @@ export interface LightbridgeRuntimeOptions {
17
33
  logger?: Logger;
18
34
  fetchImpl?: typeof fetch;
19
35
  onAuthorizationUrl?: (url: string) => Promise<void> | void;
20
- /** Override the cache root (defaults to the OS cache dir convention). */
36
+ /** Override the cache ROOT (defaults to the OS cache dir convention) — both
37
+ * the shared root-token store and the exchange-only store are joined from
38
+ * this. Test seam; production callers omit it. */
21
39
  cacheDir?: string;
22
40
  tokenExpirySkewMs?: number;
41
+ /**
42
+ * Whether to perform the RFC 8693 exchange (ADR-0012) rather than use the
43
+ * IdP access token directly as the bearer. Defaults to `false` (ADR-0017).
44
+ */
45
+ exchange?: boolean;
23
46
  }
24
47
  /**
25
48
  * The minimal surface `opencode.ts` needs from the shared runtime — narrow on
@@ -38,50 +61,81 @@ export interface LightbridgeRuntimeLike {
38
61
  */
39
62
  export type LightbridgeRuntimeFactory = (auth: AuthServerConfig, projectId: string | undefined, options: LightbridgeRuntimeOptions) => LightbridgeRuntimeLike;
40
63
  /**
41
- * The ONE shared `TokenRuntime` the umbrella plugin builds (ADR-0012): a
42
- * single human login, reused as the subject token for a project-scoped RFC
43
- * 8693 exchange whose result is consumed by BOTH the gateway (`chat.headers`)
44
- * and OTEL (`TokenSource.headers()`) — that sharing is the entire point of
45
- * the plugin. Lifecycle mirrors `@vymalo/opencode-repo-auth`'s "model b"
46
- * (`RepoAuthPlugin.resolveProjectToken`): auth-core has no higher-level
47
- * primitive for this yet, so the umbrella keeps its own thin copy rather than
48
- * reaching into a sibling package's internals.
64
+ * The ONE shared runtime the umbrella plugin builds (ADR-0012, amended by
65
+ * ADR-0017): a single human login, reused as the subject token for a
66
+ * project-scoped RFC 8693 exchange (when `gateway.exchange` is `true`) or
67
+ * used directly as the gateway/OTEL bearer (the new default) — consumed by
68
+ * BOTH the gateway (`chat.headers`) and OTEL (`TokenSource.headers()`).
69
+ *
70
+ * The human root token itself is no longer lightbridge-exclusive storage: it
71
+ * lives in the SAME cache file `@vymalo/opencode-oauth2` writes for a server
72
+ * of this id (`ROOT_CACHE_SEGMENT`/`ROOT_CACHE_NAMESPACE`), so configuring
73
+ * both plugins against the same IdP (`auth.id` matching an oauth2
74
+ * `servers[].id`, same issuer/clientId) means logging in once. An upgrading
75
+ * install's pre-existing root token is migrated in automatically — see
76
+ * `migration.ts`.
49
77
  */
50
78
  export declare class LightbridgeRuntime implements LightbridgeRuntimeLike {
51
79
  private readonly projectId;
52
- private readonly runtime;
80
+ private readonly rootRuntime;
81
+ private readonly sharedStore;
82
+ private readonly exchangeRuntime;
53
83
  private readonly logger;
54
84
  private readonly tokenExpirySkewMs;
85
+ private readonly exchange;
86
+ private readonly cacheRoot;
87
+ private readonly serverId;
55
88
  /** Token-cache key: the project id, or `DEFAULT_PROJECT_KEY` when unset. */
56
89
  private readonly projectKey;
57
90
  private inFlightExchange?;
91
+ private migration?;
58
92
  constructor(auth: AuthServerConfig, projectId: string | undefined, options?: LightbridgeRuntimeOptions);
93
+ /**
94
+ * One-time (per instance) migration of a pre-ADR-0017 root token into the
95
+ * shared cache file — see `migration.ts`. Idempotent and cheap on every
96
+ * call after the first (the underlying function also short-circuits once
97
+ * the new location has ANY state).
98
+ */
99
+ private ensureMigrated;
59
100
  /** Non-network read of the cached project token, if any. */
60
101
  getCachedProjectToken(): Promise<TokenSet | undefined>;
61
102
  /**
62
- * Resolve a usable project token, shared by every caller (gateway
63
- * `chat.headers`, OTEL `TokenSource.headers()`):
103
+ * Resolve a usable gateway/OTEL bearer, shared by every caller (gateway
104
+ * `chat.headers`, OTEL `TokenSource.headers()`).
105
+ *
106
+ * `gateway.exchange: false` (default, ADR-0017): the IdP access token IS
107
+ * the bearer — just `ensure()` the shared root token (refresh-only unless
108
+ * `interactive`).
109
+ *
110
+ * `gateway.exchange: true` (ADR-0012, unchanged):
64
111
  *
65
- * cached usable? ──yes──▶ return it
112
+ * cached exchanged token usable? ──yes──▶ return it
66
113
  * │ no
67
114
  * ▼
68
- * ensure human root (refresh-only unless `interactive`)
115
+ * ensure shared human root (refresh-only unless `interactive`)
69
116
  * ▼
70
117
  * exchangeTo(projectKey, humanToken, projectId ? { project_id } : {})
71
118
  * ▼ (no projectId → backend picks the default project)
72
119
  * cache + return
73
120
  *
74
- * Concurrent callers (a chat request racing an OTEL export) share the
75
- * in-flight exchange — at most one exchange POST per cache-miss window.
76
- * Never invents a token: an exchange failure propagates to the caller,
77
- * which is expected to fail closed (no header / empty `headers()`).
121
+ * Concurrent callers share the in-flight exchange in either mode. Never
122
+ * invents a token: a failure propagates to the caller, which is expected to
123
+ * fail closed (no header / empty `headers()`).
78
124
  */
79
125
  getProjectToken(options?: {
80
126
  interactive?: boolean;
81
127
  }): Promise<TokenSet>;
82
128
  private performExchange;
83
- /** Drop the on-disk human + project tokens for this identity. */
129
+ /**
130
+ * Drop the cached tokens for this identity: the exchanged project token
131
+ * entirely (its own exclusive store), and the shared root token's `token`
132
+ * field only — NOT the whole shared cache file, which may also hold
133
+ * `@vymalo/opencode-oauth2`'s discovered models for this server id and must
134
+ * survive a lightbridge-side logout.
135
+ */
84
136
  reset(): Promise<void>;
85
137
  }
86
- /** Absolute path to the lightbridge cache directory (for diagnostics / tests). */
138
+ /** Absolute path to the lightbridge-exclusive (exchanged-token) cache directory. */
87
139
  export declare function lightbridgeCacheDir(cacheRoot: string): string;
140
+ /** Absolute path to the SHARED root-token cache directory (ADR-0017). */
141
+ export declare function rootCacheDir(cacheRoot: string): string;
package/dist/plugin.js CHANGED
@@ -1,13 +1,31 @@
1
1
  import { join } from "node:path";
2
2
  import { createJsonConsoleLogger, DEFAULT_TOKEN_EXPIRY_SKEW_MS, resolveCacheRoot, TokenRuntime } from "@vymalo/opencode-auth-core/lib";
3
+ import { FileCacheStore as SharedFileCacheStore } from "@vymalo/opencode-provider-sync/lib";
4
+ import { migrateRootTokenIfNeeded } from "./migration.js";
3
5
  /**
4
- * Identity for the umbrella's single `TokenRuntime` (ADR-0012). Constant
5
- * because v1 is single-IdP, same rationale as repo-auth's `HUMAN_IDENTITY`.
6
+ * Legacy identity for the exchanged project-token store (UNCHANGED by
7
+ * ADR-0017 — see `migration.ts`'s module doc for why only the ROOT token
8
+ * relocated). Kept as a constant, not `auth.id`, because the exchanged-token
9
+ * cache key has always been derived from this literal, independent of the
10
+ * configured IdP's `id`.
6
11
  */
7
12
  export const LIGHTBRIDGE_IDENTITY = "lightbridge";
8
- /** Own cache namespace — separate from oauth2/repo-auth/otel's stores. */
13
+ /** Own cache namespace for the exchanged-token store — separate from oauth2/repo-auth/otel's. */
9
14
  export const DEFAULT_CACHE_NAMESPACE = "opencode-lightbridge";
10
15
  /**
16
+ * ADR-0017: the human ROOT token now lives in the SAME cache file
17
+ * `@vymalo/opencode-oauth2` uses for a server of this id —
18
+ * `<cacheRoot>/opencode-oauth2/opencode-oauth2-model-sync/<id>.json`, a fused
19
+ * `CachedServerState` (token + discovered models). This is what lets a
20
+ * developer who configures the same IdP (`auth.id` == an oauth2 `servers[].id`,
21
+ * same issuer/clientId) log in ONCE and have both plugins read/write the same
22
+ * on-disk state. The literals are pinned here (not imported from
23
+ * `@vymalo/opencode-oauth2`, which lightbridge does not depend on) — see
24
+ * `docs/adr/0017-lightbridge-all-in-one.md` for the shared-contract rationale.
25
+ */
26
+ export const ROOT_CACHE_SEGMENT = "opencode-oauth2";
27
+ export const ROOT_CACHE_NAMESPACE = "opencode-oauth2-model-sync";
28
+ /**
11
29
  * Cache key for the default-project token — used when no `projectId` is
12
30
  * configured, so the exchange sends no `project_id` and the backend mints a
13
31
  * token for the caller's default project (ADR-0012). Distinct from any real
@@ -15,10 +33,9 @@ export const DEFAULT_CACHE_NAMESPACE = "opencode-lightbridge";
15
33
  */
16
34
  export const DEFAULT_PROJECT_KEY = "__default__";
17
35
  /**
18
- * Whether a cached project token is usable. Mirrors repo-auth's
19
- * `isProjectTokenUsable`: the project token carries no refresh token, so an
20
- * undefined `expiresAt` must be treated as expired (re-exchange), never as
21
- * non-expiring.
36
+ * Whether a cached project token is usable. The project token carries no
37
+ * refresh token, so an undefined `expiresAt` must be treated as expired
38
+ * (re-exchange), never as non-expiring.
22
39
  */
23
40
  function isProjectTokenUsable(token, skewMs) {
24
41
  if (!token?.accessToken) {
@@ -30,59 +47,148 @@ function isProjectTokenUsable(token, skewMs) {
30
47
  return Date.now() + skewMs < token.expiresAt;
31
48
  }
32
49
  /**
33
- * The ONE shared `TokenRuntime` the umbrella plugin builds (ADR-0012): a
34
- * single human login, reused as the subject token for a project-scoped RFC
35
- * 8693 exchange whose result is consumed by BOTH the gateway (`chat.headers`)
36
- * and OTEL (`TokenSource.headers()`) — that sharing is the entire point of
37
- * the plugin. Lifecycle mirrors `@vymalo/opencode-repo-auth`'s "model b"
38
- * (`RepoAuthPlugin.resolveProjectToken`): auth-core has no higher-level
39
- * primitive for this yet, so the umbrella keeps its own thin copy rather than
40
- * reaching into a sibling package's internals.
50
+ * Build the ONE shared root `TokenRuntime` for `serverId`, persisting through
51
+ * the SAME fused `CachedServerState` shape+location `ProviderModelSyncEngine`
52
+ * uses (see `ROOT_CACHE_SEGMENT`/`ROOT_CACHE_NAMESPACE` above) — mirroring the
53
+ * override pattern in `@vymalo/opencode-provider-sync`'s
54
+ * `ProviderModelSyncEngine.buildTokenRuntime` (extract `.token`, preserve
55
+ * `.models`/`.rawModels` on write) but WITHOUT running that engine's
56
+ * scheduler/model-discovery — this is a read/refresh-only participant in the
57
+ * shared cache file, not a second poller (see ADR-0017's double-scheduling
58
+ * guard, enforced inside the engine itself for the `register` path).
59
+ *
60
+ * Unlike `ProviderModelSyncEngine`, this performs no cross-process
61
+ * "adopt-if-newer" reconciliation — every `getCached()` call here reads disk
62
+ * fresh and takes it as-is. That is deliberately simpler (no in-memory
63
+ * snapshot to reconcile against) and still safe: a momentarily unreadable/
64
+ * missing file degrades to "no cached token" (a safe, if suboptimal,
65
+ * fallback), and refreshes are still coordinated via `TokenRuntime`'s own
66
+ * cross-process file lock.
67
+ */
68
+ function buildSharedRootTokenRuntime(serverId, auth, sharedStore, options) {
69
+ return new TokenRuntime(serverId, auth, {
70
+ logger: options.logger,
71
+ fetchImpl: options.fetchImpl,
72
+ onAuthorizationUrl: options.onAuthorizationUrl,
73
+ tokenExpirySkewMs: options.tokenExpirySkewMs,
74
+ serviceLabel: "opencode-lightbridge",
75
+ getCached: async () => {
76
+ const state = await sharedStore.loadServerState(serverId);
77
+ return state?.token;
78
+ },
79
+ setCached: async (token) => {
80
+ const existing = await sharedStore.loadServerState(serverId);
81
+ const next = {
82
+ serverId,
83
+ updatedAt: Date.now(),
84
+ lastSyncAt: existing?.lastSyncAt,
85
+ models: existing?.models ?? [],
86
+ rawModels: existing?.rawModels ?? [],
87
+ token
88
+ };
89
+ await sharedStore.saveServerState(next);
90
+ }
91
+ });
92
+ }
93
+ /**
94
+ * The ONE shared runtime the umbrella plugin builds (ADR-0012, amended by
95
+ * ADR-0017): a single human login, reused as the subject token for a
96
+ * project-scoped RFC 8693 exchange (when `gateway.exchange` is `true`) or
97
+ * used directly as the gateway/OTEL bearer (the new default) — consumed by
98
+ * BOTH the gateway (`chat.headers`) and OTEL (`TokenSource.headers()`).
99
+ *
100
+ * The human root token itself is no longer lightbridge-exclusive storage: it
101
+ * lives in the SAME cache file `@vymalo/opencode-oauth2` writes for a server
102
+ * of this id (`ROOT_CACHE_SEGMENT`/`ROOT_CACHE_NAMESPACE`), so configuring
103
+ * both plugins against the same IdP (`auth.id` matching an oauth2
104
+ * `servers[].id`, same issuer/clientId) means logging in once. An upgrading
105
+ * install's pre-existing root token is migrated in automatically — see
106
+ * `migration.ts`.
41
107
  */
42
108
  export class LightbridgeRuntime {
43
109
  projectId;
44
- runtime;
110
+ rootRuntime;
111
+ sharedStore;
112
+ exchangeRuntime;
45
113
  logger;
46
114
  tokenExpirySkewMs;
115
+ exchange;
116
+ cacheRoot;
117
+ serverId;
47
118
  /** Token-cache key: the project id, or `DEFAULT_PROJECT_KEY` when unset. */
48
119
  projectKey;
49
120
  inFlightExchange;
121
+ migration;
50
122
  constructor(auth, projectId, options = {}) {
51
123
  this.projectId = projectId;
124
+ this.serverId = auth.id;
52
125
  this.projectKey = projectId ?? DEFAULT_PROJECT_KEY;
53
126
  this.logger = options.logger ?? createJsonConsoleLogger("info");
127
+ this.exchange = options.exchange ?? false;
128
+ this.cacheRoot = options.cacheDir ?? resolveCacheRoot();
54
129
  this.tokenExpirySkewMs = typeof options.tokenExpirySkewMs === "number" && Number.isFinite(options.tokenExpirySkewMs) && options.tokenExpirySkewMs > 0 ? options.tokenExpirySkewMs : DEFAULT_TOKEN_EXPIRY_SKEW_MS;
55
- this.runtime = new TokenRuntime(LIGHTBRIDGE_IDENTITY, auth, {
130
+ this.sharedStore = new SharedFileCacheStore(join(this.cacheRoot, ROOT_CACHE_SEGMENT, ROOT_CACHE_NAMESPACE), this.logger);
131
+ this.rootRuntime = buildSharedRootTokenRuntime(this.serverId, auth, this.sharedStore, options);
132
+ // The exchange-only runtime keeps its OWN dedicated, lightbridge-exclusive
133
+ // store (unchanged location/shape) — there is no oauth2 equivalent for a
134
+ // project-scoped exchanged token, so nothing to share here.
135
+ this.exchangeRuntime = new TokenRuntime(LIGHTBRIDGE_IDENTITY, auth, {
56
136
  logger: this.logger,
57
137
  fetchImpl: options.fetchImpl,
58
138
  onAuthorizationUrl: options.onAuthorizationUrl,
59
- cacheDir: options.cacheDir ?? join(resolveCacheRoot(), DEFAULT_CACHE_NAMESPACE),
60
- tokenExpirySkewMs: this.tokenExpirySkewMs
139
+ cacheDir: join(this.cacheRoot, DEFAULT_CACHE_NAMESPACE),
140
+ tokenExpirySkewMs: this.tokenExpirySkewMs,
141
+ serviceLabel: "opencode-lightbridge"
61
142
  });
62
143
  }
144
+ /**
145
+ * One-time (per instance) migration of a pre-ADR-0017 root token into the
146
+ * shared cache file — see `migration.ts`. Idempotent and cheap on every
147
+ * call after the first (the underlying function also short-circuits once
148
+ * the new location has ANY state).
149
+ */
150
+ ensureMigrated() {
151
+ if (!this.migration) {
152
+ this.migration = migrateRootTokenIfNeeded(this.cacheRoot, ROOT_CACHE_SEGMENT, ROOT_CACHE_NAMESPACE, this.serverId, this.logger).catch((error) => {
153
+ // Never fatal: worst case is a fresh login, same as a first-ever
154
+ // install. Swallow so a migration hiccup can't break normal auth.
155
+ this.logger.debug("lightbridge_root_migration_error", { error: error instanceof Error ? error.message : String(error) });
156
+ });
157
+ }
158
+ return this.migration;
159
+ }
63
160
  /** Non-network read of the cached project token, if any. */
64
161
  async getCachedProjectToken() {
65
- return this.runtime.getExchangedByKey(this.projectKey);
162
+ return this.exchangeRuntime.getExchangedByKey(this.projectKey);
66
163
  }
67
164
  /**
68
- * Resolve a usable project token, shared by every caller (gateway
69
- * `chat.headers`, OTEL `TokenSource.headers()`):
165
+ * Resolve a usable gateway/OTEL bearer, shared by every caller (gateway
166
+ * `chat.headers`, OTEL `TokenSource.headers()`).
167
+ *
168
+ * `gateway.exchange: false` (default, ADR-0017): the IdP access token IS
169
+ * the bearer — just `ensure()` the shared root token (refresh-only unless
170
+ * `interactive`).
171
+ *
172
+ * `gateway.exchange: true` (ADR-0012, unchanged):
70
173
  *
71
- * cached usable? ──yes──▶ return it
174
+ * cached exchanged token usable? ──yes──▶ return it
72
175
  * │ no
73
176
  * ▼
74
- * ensure human root (refresh-only unless `interactive`)
177
+ * ensure shared human root (refresh-only unless `interactive`)
75
178
  * ▼
76
179
  * exchangeTo(projectKey, humanToken, projectId ? { project_id } : {})
77
180
  * ▼ (no projectId → backend picks the default project)
78
181
  * cache + return
79
182
  *
80
- * Concurrent callers (a chat request racing an OTEL export) share the
81
- * in-flight exchange — at most one exchange POST per cache-miss window.
82
- * Never invents a token: an exchange failure propagates to the caller,
83
- * which is expected to fail closed (no header / empty `headers()`).
183
+ * Concurrent callers share the in-flight exchange in either mode. Never
184
+ * invents a token: a failure propagates to the caller, which is expected to
185
+ * fail closed (no header / empty `headers()`).
84
186
  */
85
187
  async getProjectToken(options = {}) {
188
+ await this.ensureMigrated();
189
+ if (!this.exchange) {
190
+ return this.rootRuntime.ensure({ interactive: options.interactive });
191
+ }
86
192
  const cached = await this.getCachedProjectToken();
87
193
  if (isProjectTokenUsable(cached, this.tokenExpirySkewMs)) {
88
194
  this.logger.trace("lightbridge_exchange_cache_hit", { projectId: this.projectId });
@@ -103,12 +209,12 @@ export class LightbridgeRuntime {
103
209
  }
104
210
  }
105
211
  async performExchange(options) {
106
- const human = await this.runtime.ensure({ interactive: options.interactive });
212
+ const human = await this.rootRuntime.ensure({ interactive: options.interactive });
107
213
  this.logger.info("lightbridge_exchange_started", { projectId: this.projectId ?? "(default)" });
108
214
  try {
109
215
  // No `projectId` → send no `project_id` param; the backend mints a token
110
216
  // for the caller's default project (ADR-0012).
111
- const exchanged = await this.runtime.exchangeTo(this.projectKey, human.accessToken, this.projectId ? { project_id: this.projectId } : {});
217
+ const exchanged = await this.exchangeRuntime.exchangeTo(this.projectKey, human.accessToken, this.projectId ? { project_id: this.projectId } : {});
112
218
  this.logger.info("lightbridge_exchange_success", { projectId: this.projectId ?? "(default)" });
113
219
  return exchanged;
114
220
  } catch (error) {
@@ -119,14 +225,32 @@ export class LightbridgeRuntime {
119
225
  throw error;
120
226
  }
121
227
  }
122
- /** Drop the on-disk human + project tokens for this identity. */
228
+ /**
229
+ * Drop the cached tokens for this identity: the exchanged project token
230
+ * entirely (its own exclusive store), and the shared root token's `token`
231
+ * field only — NOT the whole shared cache file, which may also hold
232
+ * `@vymalo/opencode-oauth2`'s discovered models for this server id and must
233
+ * survive a lightbridge-side logout.
234
+ */
123
235
  async reset() {
124
- await this.runtime.reset();
236
+ await this.exchangeRuntime.reset();
237
+ const existing = await this.sharedStore.loadServerState(this.serverId);
238
+ if (existing?.token) {
239
+ await this.sharedStore.saveServerState({
240
+ ...existing,
241
+ updatedAt: Date.now(),
242
+ token: undefined
243
+ });
244
+ }
125
245
  }
126
246
  }
127
- /** Absolute path to the lightbridge cache directory (for diagnostics / tests). */
247
+ /** Absolute path to the lightbridge-exclusive (exchanged-token) cache directory. */
128
248
  export function lightbridgeCacheDir(cacheRoot) {
129
249
  return join(cacheRoot, DEFAULT_CACHE_NAMESPACE);
130
250
  }
251
+ /** Absolute path to the SHARED root-token cache directory (ADR-0017). */
252
+ export function rootCacheDir(cacheRoot) {
253
+ return join(cacheRoot, ROOT_CACHE_SEGMENT, ROOT_CACHE_NAMESPACE);
254
+ }
131
255
 
132
256
  //# sourceMappingURL=plugin.js.map
@@ -1 +1 @@
1
- {"mappings":"AAAA,SAAS,YAAY;AAErB,SACE,yBACA,8BACA,kBACA,oBAIK;;;;;AAMP,OAAO,MAAM,uBAAuB;;AAGpC,OAAO,MAAM,0BAA0B;;;;;;;AAQvC,OAAO,MAAM,sBAAsB;;;;;;;AAqCnC,SAAS,qBAAqB,OAA6B,QAAmC;CAC5F,IAAI,CAAC,OAAO,aAAa;EACvB,OAAO;CACT;CACA,IAAI,MAAM,cAAc,WAAW;EACjC,OAAO;CACT;CACA,OAAO,KAAK,IAAI,IAAI,SAAS,MAAM;AACrC;;;;;;;;;;;AAYA,OAAO,MAAM,mBAAqD;CAU7C;CATnB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;;CAEjB,AAAiB;CACjB,AAAQ;CAER,YACE,MACA,AAAiB,WACjB,UAAqC,CAAC,GACtC;EAFiB;EAGjB,KAAK,aAAa,aAAa;EAC/B,KAAK,SAAS,QAAQ,UAAU,wBAAwB,MAAM;EAC9D,KAAK,oBACH,OAAO,QAAQ,sBAAsB,YACrC,OAAO,SAAS,QAAQ,iBAAiB,KACzC,QAAQ,oBAAoB,IACxB,QAAQ,oBACR;EAEN,KAAK,UAAU,IAAI,aAAa,sBAAsB,MAAM;GAC1D,QAAQ,KAAK;GACb,WAAW,QAAQ;GACnB,oBAAoB,QAAQ;GAC5B,UAAU,QAAQ,YAAY,KAAK,iBAAiB,GAAG,uBAAuB;GAC9E,mBAAmB,KAAK;EAC1B,CAAC;CACH;;CAGA,MAAM,wBAAuD;EAC3D,OAAO,KAAK,QAAQ,kBAAkB,KAAK,UAAU;CACvD;;;;;;;;;;;;;;;;;;;CAoBA,MAAM,gBAAgB,UAAqC,CAAC,GAAsB;EAChF,MAAM,SAAS,MAAM,KAAK,sBAAsB;EAChD,IAAI,qBAAqB,QAAQ,KAAK,iBAAiB,GAAG;GACxD,KAAK,OAAO,MAAM,kCAAkC,EAAE,WAAW,KAAK,UAAU,CAAC;GACjF,OAAO;EACT;EACA,KAAK,OAAO,MAAM,mCAAmC,EAAE,WAAW,KAAK,UAAU,CAAC;EAElF,IAAI,KAAK,kBAAkB;GACzB,OAAO,KAAK;EACd;EAEA,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,KAAK,mBAAmB;EACxB,IAAI;GACF,OAAO,MAAM;EACf,UAAU;GACR,IAAI,KAAK,qBAAqB,UAAU;IACtC,KAAK,mBAAmB;GAC1B;EACF;CACF;CAEA,MAAc,gBAAgB,SAAuD;EACnF,MAAM,QAAQ,MAAM,KAAK,QAAQ,OAAO,EAAE,aAAa,QAAQ,YAAY,CAAC;EAC5E,KAAK,OAAO,KAAK,gCAAgC,EAAE,WAAW,KAAK,aAAa,YAAY,CAAC;EAC7F,IAAI;;;GAGF,MAAM,YAAY,MAAM,KAAK,QAAQ,WACnC,KAAK,YACL,MAAM,aACN,KAAK,YAAY,EAAE,YAAY,KAAK,UAAU,IAAI,CAAC,CACrD;GACA,KAAK,OAAO,KAAK,gCAAgC,EAC/C,WAAW,KAAK,aAAa,YAC/B,CAAC;GACD,OAAO;EACT,SAAS,OAAO;GACd,KAAK,OAAO,MAAM,+BAA+B;IAC/C,WAAW,KAAK;IAChB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;GACD,MAAM;EACR;CACF;;CAGA,MAAM,QAAuB;EAC3B,MAAM,KAAK,QAAQ,MAAM;CAC3B;AACF;;AAGA,OAAO,SAAS,oBAAoB,WAA2B;CAC7D,OAAO,KAAK,WAAW,uBAAuB;AAChD","names":[],"sources":["../src/plugin.ts"],"version":3,"file":"plugin.js","sourceRoot":""}
1
+ {"mappings":"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","names":[],"sources":["../src/plugin.ts"],"version":3,"file":"plugin.js","sourceRoot":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vymalo/opencode-lightbridge",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Umbrella OpenCode plugin for ADR-0012: one shared credential (TokenRuntime) drives both the LLM gateway bearer and OTEL export, so a developer authenticates once and every egress rides the same project-scoped token.",
5
5
  "license": "MIT",
6
6
  "author": "vymalo contributors",
@@ -53,8 +53,9 @@
53
53
  "@opencode-ai/plugin": "1.15.10",
54
54
  "@opentelemetry/api": "^1.9.1",
55
55
  "@opentelemetry/api-logs": "^0.221.0",
56
- "@vymalo/opencode-auth-core": "0.16.1",
57
- "@vymalo/opencode-core-otel": "0.16.1"
56
+ "@vymalo/opencode-auth-core": "0.17.0",
57
+ "@vymalo/opencode-core-otel": "0.17.0",
58
+ "@vymalo/opencode-provider-sync": "0.17.0"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@opentelemetry/sdk-logs": "^0.221.0",