@vymalo/opencode-lightbridge 0.16.1 → 0.18.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.
@@ -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":""}
@@ -1,9 +1,19 @@
1
- import type { Hooks, Plugin } from "@opencode-ai/plugin";
2
- import { type Logger } from "@vymalo/opencode-auth-core/lib";
1
+ import { Plugin } from "@opencode/plugin/effect";
3
2
  import { type EnvSource, type ExporterFactories } from "@vymalo/opencode-core-otel";
3
+ import { ProviderModelSyncEngine } from "@vymalo/opencode-provider-sync/lib";
4
+ import { type Logger } from "./logging.js";
4
5
  import { type LightbridgeRuntimeFactory } from "./plugin.js";
5
- type OpenCodeConfig = Parameters<NonNullable<Hooks["config"]>>[0];
6
+ /** Stable plugin id. v2 requires one and keys storage/registrations by it. */
7
+ export declare const PLUGIN_ID = "vymalo.lightbridge";
8
+ /** Test/embedder handle onto the `register` module's background engine. */
9
+ export interface LightbridgeRegisterHandle {
10
+ /** Resolves once every queued engine build + warmup (and its reload) has finished. */
11
+ settled(): Promise<void>;
12
+ /** The current engine, or `undefined` before it is ready / after a collision stand-down. */
13
+ engine(): ProviderModelSyncEngine | undefined;
14
+ }
6
15
  export interface LightbridgePluginFactoryOptions {
16
+ /** Override the logger (tests). Defaults to the host's Effect logger. */
7
17
  logger?: Logger;
8
18
  /** Shared-runtime HTTP client (auth-core `TokenRuntime`). Tests only. */
9
19
  fetchImpl?: typeof fetch;
@@ -17,13 +27,15 @@ export interface LightbridgePluginFactoryOptions {
17
27
  * neither module ever touches the network.
18
28
  */
19
29
  runtimeFactory?: LightbridgeRuntimeFactory;
30
+ /** Observe the `register` module's background engine (tests). */
31
+ onRegister?: (handle: LightbridgeRegisterHandle) => void;
20
32
  /** Injected environment; defaults to `process.env`. */
21
33
  env?: EnvSource;
22
34
  /** Substitute exporters (tests use in-memory ones). */
23
35
  exporters?: ExporterFactories;
24
36
  /** Injectable clock; defaults to `Date.now`. */
25
37
  now?: () => number;
26
- /** Skip `beforeExit`/`SIGINT`/`SIGTERM` registration (tests). */
38
+ /** Skip the `beforeExit`/`SIGINT`/`SIGTERM` exit handlers (tests). See ADR-0018. */
27
39
  registerProcessHandlers?: boolean;
28
40
  /** Override the resolved host metadata (hostname, version) for tests. */
29
41
  hostInfo?: {
@@ -34,16 +46,22 @@ export interface LightbridgePluginFactoryOptions {
34
46
  deferredTimeoutMs?: number;
35
47
  }
36
48
  /**
37
- * Create the `@vymalo/opencode-lightbridge` umbrella plugin (ADR-0012): ONE
38
- * shared `TokenRuntime`, built once from `auth`, drives both the gateway
39
- * bearer (`gateway`) and the OTEL export credential (`otel`) — each module
40
- * activates independently, and an `auth`-only config is a valid, inert
41
- * plugin. `config` runs both modules' config-time logic (host log level +
42
- * OTEL trace propagation); `chat.headers` is the gateway injector;
43
- * `event`/`chat.message`/`tool.execute.*`/`chat.params`/`permission.ask`/
44
- * `experimental.*` are the OTEL observers.
49
+ * Create the `@vymalo/opencode-lightbridge` umbrella plugin (ADR-0012,
50
+ * ADR-0017) for the OpenCode v2 plugin API (ADR-0019): ONE shared
51
+ * `TokenRuntime`, built once from `auth`, drives both the gateway bearer
52
+ * (`gateway`) and the OTEL export credential (`otel`) — each module activates
53
+ * independently, and an `auth`-only config is a valid, inert plugin.
54
+ *
55
+ * - `register`: `provider.transform` registers the provider and merges the
56
+ * discovered models and cached bearer; a queued background build runs the
57
+ * `ProviderModelSyncEngine` and calls `provider.reload()` (the transform is
58
+ * synchronous and cheap by contract — same shape as oauth2).
59
+ * - `gateway`: `session.hook("model.request")` injects the shared bearer and
60
+ * fails closed by dying the request when no bearer can be produced.
61
+ * - `otel`: `event.subscribe` + `prompt`/`context`/`tool`/`permission` hooks
62
+ * feed the recorder through `otel-adapter.ts`; `http.request` carries W3C
63
+ * trace context.
45
64
  */
46
- export declare function createLightbridgePlugin(factoryOptions?: LightbridgePluginFactoryOptions): Plugin;
47
- export declare const LightbridgePlugin: Plugin;
65
+ export declare function createLightbridgePlugin(factoryOptions?: LightbridgePluginFactoryOptions): Plugin.Plugin;
66
+ export declare const LightbridgePlugin: Plugin.Plugin;
48
67
  export default LightbridgePlugin;
49
- export type { OpenCodeConfig };