@jami-studio/core 0.92.26 → 0.92.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +2 -1
  4. package/corpus/core/src/deploy/build.ts +56 -2
  5. package/corpus/core/src/event-bus/bus.ts +138 -140
  6. package/corpus/core/src/event-bus/registry.ts +81 -79
  7. package/corpus/core/src/file-upload/registry.ts +135 -125
  8. package/corpus/core/src/notifications/registry.ts +218 -216
  9. package/corpus/core/src/observability/store.ts +1313 -1321
  10. package/corpus/core/src/private-blob/registry.ts +246 -242
  11. package/corpus/core/src/secrets/register.ts +132 -129
  12. package/corpus/core/src/shared/global-scope.ts +75 -0
  13. package/corpus/core/src/shared/init-memo.ts +94 -0
  14. package/corpus/core/src/sharing/registry.ts +193 -194
  15. package/corpus/core/src/tracking/providers.ts +425 -422
  16. package/corpus/core/src/tracking/registry.ts +102 -102
  17. package/dist/collab/routes.d.ts +1 -1
  18. package/dist/deploy/build.d.ts +23 -0
  19. package/dist/deploy/build.d.ts.map +1 -1
  20. package/dist/deploy/build.js +36 -2
  21. package/dist/deploy/build.js.map +1 -1
  22. package/dist/event-bus/bus.d.ts.map +1 -1
  23. package/dist/event-bus/bus.js +8 -6
  24. package/dist/event-bus/bus.js.map +1 -1
  25. package/dist/event-bus/registry.d.ts.map +1 -1
  26. package/dist/event-bus/registry.js +10 -6
  27. package/dist/event-bus/registry.js.map +1 -1
  28. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  29. package/dist/file-upload/registry.d.ts.map +1 -1
  30. package/dist/file-upload/registry.js +28 -8
  31. package/dist/file-upload/registry.js.map +1 -1
  32. package/dist/notifications/registry.d.ts.map +1 -1
  33. package/dist/notifications/registry.js +6 -5
  34. package/dist/notifications/registry.js.map +1 -1
  35. package/dist/observability/routes.d.ts +5 -5
  36. package/dist/observability/store.d.ts +1 -1
  37. package/dist/observability/store.d.ts.map +1 -1
  38. package/dist/observability/store.js +311 -316
  39. package/dist/observability/store.js.map +1 -1
  40. package/dist/private-blob/registry.d.ts.map +1 -1
  41. package/dist/private-blob/registry.js +18 -13
  42. package/dist/private-blob/registry.js.map +1 -1
  43. package/dist/progress/routes.d.ts +1 -1
  44. package/dist/resources/handlers.d.ts +2 -2
  45. package/dist/secrets/register.d.ts.map +1 -1
  46. package/dist/secrets/register.js +11 -7
  47. package/dist/secrets/register.js.map +1 -1
  48. package/dist/secrets/routes.d.ts +9 -9
  49. package/dist/shared/global-scope.d.ts +55 -0
  50. package/dist/shared/global-scope.d.ts.map +1 -0
  51. package/dist/shared/global-scope.js +70 -0
  52. package/dist/shared/global-scope.js.map +1 -0
  53. package/dist/shared/init-memo.d.ts +34 -0
  54. package/dist/shared/init-memo.d.ts.map +1 -0
  55. package/dist/shared/init-memo.js +80 -0
  56. package/dist/shared/init-memo.js.map +1 -0
  57. package/dist/sharing/registry.d.ts.map +1 -1
  58. package/dist/sharing/registry.js +2 -16
  59. package/dist/sharing/registry.js.map +1 -1
  60. package/dist/tracking/providers.d.ts.map +1 -1
  61. package/dist/tracking/providers.js +11 -9
  62. package/dist/tracking/providers.js.map +1 -1
  63. package/dist/tracking/registry.d.ts.map +1 -1
  64. package/dist/tracking/registry.js +5 -5
  65. package/dist/tracking/registry.js.map +1 -1
  66. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/secrets/register.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiDH,6EAA6E;AAC7E,qEAAqE;AACrE,wEAAwE;AACxE,uEAAuE;AACvE,sEAAsE;AACtE,mDAAmD;AACnD,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;AAIvE,MAAM,QAAQ,GAAkC,CAC9C,UACD,CAAC,YAAY,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAwB;IAC7D,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IACE,MAAM,CAAC,KAAK,KAAK,MAAM;QACvB,MAAM,CAAC,KAAK,KAAK,WAAW;QAC5B,MAAM,CAAC,KAAK,KAAK,KAAK,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,oFAAoF,MAAM,CAAC,KAAK,IAAI,CACrG,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,0EAA0E,MAAM,CAAC,IAAI,IAAI,CAC1F,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAClD,OAAO,CAAC,GAAG,CACT,gDAAgD,MAAM,CAAC,GAAG,0BAA0B,CACrF,CAAC;IACJ,CAAC;IACD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEjC,wEAAwE;IACxE,yEAAyE;IACzE,oCAAoC;IACpC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,yEAAyE;QACzE,kEAAkE;QAClE,MAAM,CAAC,iBAAiB,CAAC;aACtB,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,iCAAiC,CAAC,MAAM,CAAC,CAAC;aAC5D,KAAK,CAAC,GAAG,EAAE;YACV,4DAA4D;QAC9D,CAAC,CAAC,CAAC;IACP,CAAC;AACH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,mBAAmB;IACjC,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,sBAAsB;IACpC,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC","sourcesContent":["/**\r\n * In-process registry of required / optional secrets.\r\n *\r\n * Templates call `registerRequiredSecret()` at module load time — typically\r\n * from a server plugin. The secrets HTTP routes and the sidebar settings UI\r\n * read from this registry on every request so overrides and late-registered\r\n * secrets are picked up without a restart.\r\n */\r\n\r\nexport type SecretScope = \"user\" | \"workspace\" | \"org\";\r\nexport type SecretKind = \"api-key\" | \"oauth\";\r\n\r\nexport interface ValidatorResult {\r\n ok: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SecretValidator {\r\n (\r\n value: string,\r\n ): Promise<ValidatorResult | boolean> | ValidatorResult | boolean;\r\n}\r\n\r\nexport interface RegisteredSecret {\r\n /** Env var name & settings key — e.g. \"OPENAI_API_KEY\". */\r\n key: string;\r\n /** Human-readable label shown in the sidebar. */\r\n label: string;\r\n /** Short description shown below the label. */\r\n description?: string;\r\n /** URL where the user can obtain the key or connect the account. */\r\n docsUrl?: string;\r\n /** Whether the secret is per-user or shared across a workspace/org. */\r\n scope: SecretScope;\r\n /** UI affordance: \"api-key\" renders an input; \"oauth\" renders Connect. */\r\n kind: SecretKind;\r\n /** When true, an onboarding step is auto-injected for this secret. */\r\n required?: boolean;\r\n /**\r\n * Optional health check. Receives the plain-text value, returns `true` or\r\n * `{ ok: true }` on success. Returning `{ ok: false, error }` surfaces the\r\n * error to the UI. Never log the value from inside the validator.\r\n */\r\n validator?: SecretValidator;\r\n /**\r\n * For `kind: \"oauth\"` — the oauth-tokens provider id (e.g. \"google\") that\r\n * backs this registration. Used to surface OAuth status in the unified UI.\r\n */\r\n oauthProvider?: string;\r\n /**\r\n * For `kind: \"oauth\"` — URL the Connect button should point at. Typically\r\n * the framework's `/_agent-native/google/auth-url` or similar.\r\n */\r\n oauthConnectUrl?: string;\r\n}\r\n\r\n// Pin the registry to globalThis so templates that load `@agent-native/core`\r\n// via more than one ESM graph (e.g. dev-mode Vite + Nitro, symlinked\r\n// node_modules, dist/ vs src/) share a single registry. Without this, a\r\n// template's `register-secrets.ts` side-effect module may populate one\r\n// registry instance while the /_agent-native/secrets route reads from\r\n// another — net effect: the UI sees an empty list.\r\nconst REGISTRY_KEY = Symbol.for(\"@agent-native/core/secrets.registry\");\r\ninterface GlobalWithRegistry {\r\n [REGISTRY_KEY]?: Map<string, RegisteredSecret>;\r\n}\r\nconst registry: Map<string, RegisteredSecret> = ((\r\n globalThis as unknown as GlobalWithRegistry\r\n)[REGISTRY_KEY] ??= new Map());\r\n\r\n/**\r\n * Register (or override) a required secret.\r\n *\r\n * Subsequent registrations with the same `key` replace the previous\r\n * definition — later plugins can override framework defaults.\r\n */\r\nexport function registerRequiredSecret(secret: RegisteredSecret): void {\r\n if (!secret || typeof secret.key !== \"string\" || !secret.key) {\r\n throw new Error(\"registerRequiredSecret: secret.key is required\");\r\n }\r\n if (\r\n secret.scope !== \"user\" &&\r\n secret.scope !== \"workspace\" &&\r\n secret.scope !== \"org\"\r\n ) {\r\n throw new Error(\r\n `registerRequiredSecret: secret.scope must be \"user\", \"workspace\", or \"org\" (got \"${secret.scope}\")`,\r\n );\r\n }\r\n if (secret.kind !== \"api-key\" && secret.kind !== \"oauth\") {\r\n throw new Error(\r\n `registerRequiredSecret: secret.kind must be \"api-key\" or \"oauth\" (got \"${secret.kind}\")`,\r\n );\r\n }\r\n if (registry.has(secret.key) && process.env.DEBUG) {\r\n console.log(\r\n `[agent-native] Overriding registered secret \"${secret.key}\" with new registration.`,\r\n );\r\n }\r\n registry.set(secret.key, secret);\r\n\r\n // Auto-inject an onboarding step for required secrets. Done via dynamic\r\n // import to avoid a load-order cycle between register and the onboarding\r\n // registry during module bootstrap.\r\n if (secret.required) {\r\n // Lazy import — resolved synchronously in practice because the module is\r\n // already loaded once any route handler runs, but tolerate async.\r\n import(\"./onboarding.js\")\r\n .then((mod) => mod.maybeRegisterSecretOnboardingStep(secret))\r\n .catch(() => {\r\n // Onboarding is optional — never let it block registration.\r\n });\r\n }\r\n}\r\n\r\n/** Return all registered secrets in registration order. */\r\nexport function listRequiredSecrets(): RegisteredSecret[] {\r\n return Array.from(registry.values());\r\n}\r\n\r\n/** Look up a single registered secret by key. */\r\nexport function getRequiredSecret(key: string): RegisteredSecret | undefined {\r\n return registry.get(key);\r\n}\r\n\r\n/** Test helper — clears the registry between runs. */\r\nexport function __resetSecretsRegistry(): void {\r\n registry.clear();\r\n}\r\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/secrets/register.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAiD5D,6EAA6E;AAC7E,qEAAqE;AACrE,wEAAwE;AACxE,uEAAuE;AACvE,sEAAsE;AACtE,mDAAmD;AACnD,8EAA8E;AAC9E,2EAA2E;AAC3E,SAAS,kBAAkB;IACzB,OAAO,eAAe,CACpB,+BAA+B,EAC/B,GAAG,EAAE,CAAC,IAAI,GAAG,EAA4B,CAC1C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAwB;IAC7D,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IACE,MAAM,CAAC,KAAK,KAAK,MAAM;QACvB,MAAM,CAAC,KAAK,KAAK,WAAW;QAC5B,MAAM,CAAC,KAAK,KAAK,KAAK,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,oFAAoF,MAAM,CAAC,KAAK,IAAI,CACrG,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,0EAA0E,MAAM,CAAC,IAAI,IAAI,CAC1F,CAAC;IACJ,CAAC;IACD,IAAI,kBAAkB,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAC9D,OAAO,CAAC,GAAG,CACT,gDAAgD,MAAM,CAAC,GAAG,0BAA0B,CACrF,CAAC;IACJ,CAAC;IACD,kBAAkB,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE7C,wEAAwE;IACxE,yEAAyE;IACzE,oCAAoC;IACpC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,yEAAyE;QACzE,kEAAkE;QAClE,MAAM,CAAC,iBAAiB,CAAC;aACtB,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,iCAAiC,CAAC,MAAM,CAAC,CAAC;aAC5D,KAAK,CAAC,GAAG,EAAE;YACV,4DAA4D;QAC9D,CAAC,CAAC,CAAC;IACP,CAAC;AACH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,mBAAmB;IACjC,OAAO,KAAK,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,kBAAkB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,sBAAsB;IACpC,kBAAkB,EAAE,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * In-process registry of required / optional secrets.\n *\n * Templates call `registerRequiredSecret()` at module load time — typically\n * from a server plugin. The secrets HTTP routes and the sidebar settings UI\n * read from this registry on every request so overrides and late-registered\n * secrets are picked up without a restart.\n */\n\nimport { getScopedGlobal } from \"../shared/global-scope.js\";\n\nexport type SecretScope = \"user\" | \"workspace\" | \"org\";\nexport type SecretKind = \"api-key\" | \"oauth\";\n\nexport interface ValidatorResult {\n ok: boolean;\n error?: string;\n}\n\nexport interface SecretValidator {\n (\n value: string,\n ): Promise<ValidatorResult | boolean> | ValidatorResult | boolean;\n}\n\nexport interface RegisteredSecret {\n /** Env var name & settings key — e.g. \"OPENAI_API_KEY\". */\n key: string;\n /** Human-readable label shown in the sidebar. */\n label: string;\n /** Short description shown below the label. */\n description?: string;\n /** URL where the user can obtain the key or connect the account. */\n docsUrl?: string;\n /** Whether the secret is per-user or shared across a workspace/org. */\n scope: SecretScope;\n /** UI affordance: \"api-key\" renders an input; \"oauth\" renders Connect. */\n kind: SecretKind;\n /** When true, an onboarding step is auto-injected for this secret. */\n required?: boolean;\n /**\n * Optional health check. Receives the plain-text value, returns `true` or\n * `{ ok: true }` on success. Returning `{ ok: false, error }` surfaces the\n * error to the UI. Never log the value from inside the validator.\n */\n validator?: SecretValidator;\n /**\n * For `kind: \"oauth\"` — the oauth-tokens provider id (e.g. \"google\") that\n * backs this registration. Used to surface OAuth status in the unified UI.\n */\n oauthProvider?: string;\n /**\n * For `kind: \"oauth\"` — URL the Connect button should point at. Typically\n * the framework's `/_agent-native/google/auth-url` or similar.\n */\n oauthConnectUrl?: string;\n}\n\n// Pin the registry to globalThis so templates that load `@agent-native/core`\n// via more than one ESM graph (e.g. dev-mode Vite + Nitro, symlinked\n// node_modules, dist/ vs src/) share a single registry. Without this, a\n// template's `register-secrets.ts` side-effect module may populate one\n// registry instance while the /_agent-native/secrets route reads from\n// another — net effect: the UI sees an empty list.\n// Scope-aware + lazily resolved so unified workspace deployments (all apps in\n// one isolate) keep per-app secret registrations. See shared/global-scope.\nfunction getSecretsRegistry(): Map<string, RegisteredSecret> {\n return getScopedGlobal(\n \"agent-native.secrets.registry\",\n () => new Map<string, RegisteredSecret>(),\n );\n}\n\n/**\n * Register (or override) a required secret.\n *\n * Subsequent registrations with the same `key` replace the previous\n * definition — later plugins can override framework defaults.\n */\nexport function registerRequiredSecret(secret: RegisteredSecret): void {\n if (!secret || typeof secret.key !== \"string\" || !secret.key) {\n throw new Error(\"registerRequiredSecret: secret.key is required\");\n }\n if (\n secret.scope !== \"user\" &&\n secret.scope !== \"workspace\" &&\n secret.scope !== \"org\"\n ) {\n throw new Error(\n `registerRequiredSecret: secret.scope must be \"user\", \"workspace\", or \"org\" (got \"${secret.scope}\")`,\n );\n }\n if (secret.kind !== \"api-key\" && secret.kind !== \"oauth\") {\n throw new Error(\n `registerRequiredSecret: secret.kind must be \"api-key\" or \"oauth\" (got \"${secret.kind}\")`,\n );\n }\n if (getSecretsRegistry().has(secret.key) && process.env.DEBUG) {\n console.log(\n `[agent-native] Overriding registered secret \"${secret.key}\" with new registration.`,\n );\n }\n getSecretsRegistry().set(secret.key, secret);\n\n // Auto-inject an onboarding step for required secrets. Done via dynamic\n // import to avoid a load-order cycle between register and the onboarding\n // registry during module bootstrap.\n if (secret.required) {\n // Lazy import — resolved synchronously in practice because the module is\n // already loaded once any route handler runs, but tolerate async.\n import(\"./onboarding.js\")\n .then((mod) => mod.maybeRegisterSecretOnboardingStep(secret))\n .catch(() => {\n // Onboarding is optional — never let it block registration.\n });\n }\n}\n\n/** Return all registered secrets in registration order. */\nexport function listRequiredSecrets(): RegisteredSecret[] {\n return Array.from(getSecretsRegistry().values());\n}\n\n/** Look up a single registered secret by key. */\nexport function getRequiredSecret(key: string): RegisteredSecret | undefined {\n return getSecretsRegistry().get(key);\n}\n\n/** Test helper — clears the registry between runs. */\nexport function __resetSecretsRegistry(): void {\n getSecretsRegistry().clear();\n}\n"]}
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — re-run the validator against the
54
54
  * current stored value without changing anything. Useful for the "Test" button.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,12 +95,12 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
106
106
  //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Per-module-graph global scope for globalThis-pinned framework registries.
3
+ *
4
+ * Why this exists: several framework registries (file-upload providers,
5
+ * private-blob providers, shareable resources, event bus, notification
6
+ * channels, tracking providers, secrets registry) pin their state on
7
+ * `globalThis` so that multiple ESM graphs of ONE app (Vite dev + Nitro,
8
+ * symlinked node_modules, dist/ vs src/) still share a single registry.
9
+ *
10
+ * On a unified workspace deployment (Cloudflare Pages `_worker.js` dispatcher
11
+ * importing every app's worker into ONE isolate) that pinning goes too far:
12
+ * each app's bundle keeps its own module graph — so module-scope state is
13
+ * correctly per-app — but the `globalThis` pin collapses all apps' registries
14
+ * into one shared map. Real-world failure: an upload POSTed to /assets was
15
+ * served by the clips app's registered S3 provider (wrong object prefix).
16
+ *
17
+ * The fix: each app's generated worker entry calls `setGlobalScopeId(appId)`
18
+ * from a scope-init module evaluated FIRST in the entry's import graph (ESM
19
+ * evaluates imports depth-first in declaration order, so the scope is set
20
+ * before any registry module initializes or registers built-ins). Registries
21
+ * resolve their `globalThis` key LAZILY through `getScopedGlobal`, which
22
+ * namespaces the key by the module graph's scope id. Result:
23
+ *
24
+ * - Unified worker: each app's core copy has its own scope id → per-app keys
25
+ * → per-app registries. Cross-app state sharing is gone.
26
+ * - Dev / single-app deployments: `setGlobalScopeId` is never called → keys
27
+ * are unscoped → the original multi-graph dedupe behavior is preserved.
28
+ *
29
+ * This module must stay dependency-free: it is imported by the generated
30
+ * scope-init module before anything else in the app bundle evaluates.
31
+ */
32
+ /**
33
+ * Set the global-registry scope for THIS module graph (this app's bundle).
34
+ * Called by the generated worker entry's scope-init module on unified
35
+ * workspace deployments. Pass `null` to clear (tests).
36
+ */
37
+ export declare function setGlobalScopeId(id: string | null): void;
38
+ /** The active scope id for this module graph, or `null` when unscoped. */
39
+ export declare function getGlobalScopeId(): string | null;
40
+ /**
41
+ * The fully-qualified global key name for `base` under the active scope.
42
+ * Unscoped graphs get `base` unchanged, so existing dev-mode behavior
43
+ * (one registry across all of an app's ESM graphs) is preserved.
44
+ */
45
+ export declare function scopedGlobalKeyName(base: string): string;
46
+ /**
47
+ * Lazily resolve (and initialize once) a globalThis-pinned singleton under
48
+ * the scope-aware key for `base`. Registries MUST call this per access —
49
+ * never capture the result in module scope — so a scope id set during
50
+ * entry-module evaluation is honored by every later registration and read.
51
+ */
52
+ export declare function getScopedGlobal<T>(base: string, init: () => T): T;
53
+ /** Test helper — delete the pinned singleton for `base` in the ACTIVE scope. */
54
+ export declare function __deleteScopedGlobal(base: string): void;
55
+ //# sourceMappingURL=global-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-scope.d.ts","sourceRoot":"","sources":["../../src/shared/global-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGxD;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAIjE;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGvD"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Per-module-graph global scope for globalThis-pinned framework registries.
3
+ *
4
+ * Why this exists: several framework registries (file-upload providers,
5
+ * private-blob providers, shareable resources, event bus, notification
6
+ * channels, tracking providers, secrets registry) pin their state on
7
+ * `globalThis` so that multiple ESM graphs of ONE app (Vite dev + Nitro,
8
+ * symlinked node_modules, dist/ vs src/) still share a single registry.
9
+ *
10
+ * On a unified workspace deployment (Cloudflare Pages `_worker.js` dispatcher
11
+ * importing every app's worker into ONE isolate) that pinning goes too far:
12
+ * each app's bundle keeps its own module graph — so module-scope state is
13
+ * correctly per-app — but the `globalThis` pin collapses all apps' registries
14
+ * into one shared map. Real-world failure: an upload POSTed to /assets was
15
+ * served by the clips app's registered S3 provider (wrong object prefix).
16
+ *
17
+ * The fix: each app's generated worker entry calls `setGlobalScopeId(appId)`
18
+ * from a scope-init module evaluated FIRST in the entry's import graph (ESM
19
+ * evaluates imports depth-first in declaration order, so the scope is set
20
+ * before any registry module initializes or registers built-ins). Registries
21
+ * resolve their `globalThis` key LAZILY through `getScopedGlobal`, which
22
+ * namespaces the key by the module graph's scope id. Result:
23
+ *
24
+ * - Unified worker: each app's core copy has its own scope id → per-app keys
25
+ * → per-app registries. Cross-app state sharing is gone.
26
+ * - Dev / single-app deployments: `setGlobalScopeId` is never called → keys
27
+ * are unscoped → the original multi-graph dedupe behavior is preserved.
28
+ *
29
+ * This module must stay dependency-free: it is imported by the generated
30
+ * scope-init module before anything else in the app bundle evaluates.
31
+ */
32
+ let moduleGraphScopeId = null;
33
+ /**
34
+ * Set the global-registry scope for THIS module graph (this app's bundle).
35
+ * Called by the generated worker entry's scope-init module on unified
36
+ * workspace deployments. Pass `null` to clear (tests).
37
+ */
38
+ export function setGlobalScopeId(id) {
39
+ const trimmed = typeof id === "string" ? id.trim() : "";
40
+ moduleGraphScopeId = trimmed ? trimmed : null;
41
+ }
42
+ /** The active scope id for this module graph, or `null` when unscoped. */
43
+ export function getGlobalScopeId() {
44
+ return moduleGraphScopeId;
45
+ }
46
+ /**
47
+ * The fully-qualified global key name for `base` under the active scope.
48
+ * Unscoped graphs get `base` unchanged, so existing dev-mode behavior
49
+ * (one registry across all of an app's ESM graphs) is preserved.
50
+ */
51
+ export function scopedGlobalKeyName(base) {
52
+ return moduleGraphScopeId ? `${base}::app:${moduleGraphScopeId}` : base;
53
+ }
54
+ /**
55
+ * Lazily resolve (and initialize once) a globalThis-pinned singleton under
56
+ * the scope-aware key for `base`. Registries MUST call this per access —
57
+ * never capture the result in module scope — so a scope id set during
58
+ * entry-module evaluation is honored by every later registration and read.
59
+ */
60
+ export function getScopedGlobal(base, init) {
61
+ const key = Symbol.for(scopedGlobalKeyName(base));
62
+ const g = globalThis;
63
+ return (g[key] ??= init());
64
+ }
65
+ /** Test helper — delete the pinned singleton for `base` in the ACTIVE scope. */
66
+ export function __deleteScopedGlobal(base) {
67
+ const key = Symbol.for(scopedGlobalKeyName(base));
68
+ delete globalThis[key];
69
+ }
70
+ //# sourceMappingURL=global-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-scope.js","sourceRoot":"","sources":["../../src/shared/global-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,IAAI,kBAAkB,GAAkB,IAAI,CAAC;AAE7C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAiB;IAChD,MAAM,OAAO,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,kBAAkB,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB;IAC9B,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,kBAAkB,CAAC,CAAC,CAAC,GAAG,IAAI,SAAS,kBAAkB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAI,IAAY,EAAE,IAAa;IAC5D,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,GAAG,UAAsD,CAAC;IACjE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,OAAQ,UAAiD,CAAC,GAAG,CAAC,CAAC;AACjE,CAAC","sourcesContent":["/**\n * Per-module-graph global scope for globalThis-pinned framework registries.\n *\n * Why this exists: several framework registries (file-upload providers,\n * private-blob providers, shareable resources, event bus, notification\n * channels, tracking providers, secrets registry) pin their state on\n * `globalThis` so that multiple ESM graphs of ONE app (Vite dev + Nitro,\n * symlinked node_modules, dist/ vs src/) still share a single registry.\n *\n * On a unified workspace deployment (Cloudflare Pages `_worker.js` dispatcher\n * importing every app's worker into ONE isolate) that pinning goes too far:\n * each app's bundle keeps its own module graph — so module-scope state is\n * correctly per-app — but the `globalThis` pin collapses all apps' registries\n * into one shared map. Real-world failure: an upload POSTed to /assets was\n * served by the clips app's registered S3 provider (wrong object prefix).\n *\n * The fix: each app's generated worker entry calls `setGlobalScopeId(appId)`\n * from a scope-init module evaluated FIRST in the entry's import graph (ESM\n * evaluates imports depth-first in declaration order, so the scope is set\n * before any registry module initializes or registers built-ins). Registries\n * resolve their `globalThis` key LAZILY through `getScopedGlobal`, which\n * namespaces the key by the module graph's scope id. Result:\n *\n * - Unified worker: each app's core copy has its own scope id → per-app keys\n * → per-app registries. Cross-app state sharing is gone.\n * - Dev / single-app deployments: `setGlobalScopeId` is never called → keys\n * are unscoped → the original multi-graph dedupe behavior is preserved.\n *\n * This module must stay dependency-free: it is imported by the generated\n * scope-init module before anything else in the app bundle evaluates.\n */\n\nlet moduleGraphScopeId: string | null = null;\n\n/**\n * Set the global-registry scope for THIS module graph (this app's bundle).\n * Called by the generated worker entry's scope-init module on unified\n * workspace deployments. Pass `null` to clear (tests).\n */\nexport function setGlobalScopeId(id: string | null): void {\n const trimmed = typeof id === \"string\" ? id.trim() : \"\";\n moduleGraphScopeId = trimmed ? trimmed : null;\n}\n\n/** The active scope id for this module graph, or `null` when unscoped. */\nexport function getGlobalScopeId(): string | null {\n return moduleGraphScopeId;\n}\n\n/**\n * The fully-qualified global key name for `base` under the active scope.\n * Unscoped graphs get `base` unchanged, so existing dev-mode behavior\n * (one registry across all of an app's ESM graphs) is preserved.\n */\nexport function scopedGlobalKeyName(base: string): string {\n return moduleGraphScopeId ? `${base}::app:${moduleGraphScopeId}` : base;\n}\n\n/**\n * Lazily resolve (and initialize once) a globalThis-pinned singleton under\n * the scope-aware key for `base`. Registries MUST call this per access —\n * never capture the result in module scope — so a scope id set during\n * entry-module evaluation is honored by every later registration and read.\n */\nexport function getScopedGlobal<T>(base: string, init: () => T): T {\n const key = Symbol.for(scopedGlobalKeyName(base));\n const g = globalThis as unknown as Record<symbol, T | undefined>;\n return (g[key] ??= init());\n}\n\n/** Test helper — delete the pinned singleton for `base` in the ACTIVE scope. */\nexport function __deleteScopedGlobal(base: string): void {\n const key = Symbol.for(scopedGlobalKeyName(base));\n delete (globalThis as unknown as Record<symbol, unknown>)[key];\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Workerd-safe memoization for module-scope init promises.
3
+ *
4
+ * Nearly every SQL-backed store memoizes its table-creation promise at module
5
+ * scope (`let _initPromise`). On Cloudflare Workers (workerd) that pattern has
6
+ * a lethal failure mode: workerd cancels a request's pending I/O the moment
7
+ * its response returns, so an init promise CREATED during an early-responding
8
+ * request (auth probe, 404) FREEZES forever — and every later caller that
9
+ * awaits the memo hangs permanently. Proven live: `ensureObservabilityTables`
10
+ * frozen by a `get-session`-first ordering wedged every agent chat run at
11
+ * "Starting agent" on the unified Cloudflare runtime.
12
+ *
13
+ * `createInitMemo` keeps the single-flight memo semantics everywhere, and on
14
+ * workerd adds two layers of defense:
15
+ * 1. The init promise is tied to the creating request's lifetime via
16
+ * `__cf_ctx.waitUntil`, so workerd keeps its I/O alive to completion
17
+ * even when the response returns first (same remedy as the plugin-init
18
+ * freeze fix in framework-request-handler.ts).
19
+ * 2. Awaits on a still-pending memo are bounded; on timeout the memo is
20
+ * re-run under the CURRENT (live) request. Init bodies are idempotent
21
+ * (CREATE TABLE IF NOT EXISTS / guarded ALTERs), so a re-run is safe.
22
+ *
23
+ * On Node the behavior is identical to the raw memo pattern (single flight,
24
+ * failed init clears the memo so the next caller retries).
25
+ */
26
+ /** How long a pending memo may be awaited on workerd before it is presumed
27
+ * frozen and re-run. Long enough for a slow cold init (Neon DDL over HTTP),
28
+ * short enough that a frozen memo degrades to a slow call, not a hang. */
29
+ export declare const INIT_MEMO_FROZEN_RETRY_MS = 15000;
30
+ export declare function createInitMemo(init: () => Promise<void>, options?: {
31
+ frozenRetryMs?: number;
32
+ label?: string;
33
+ }): () => Promise<void>;
34
+ //# sourceMappingURL=init-memo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init-memo.d.ts","sourceRoot":"","sources":["../../src/shared/init-memo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH;;0EAE0E;AAC1E,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAIhD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EACzB,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,MAAM,OAAO,CAAC,IAAI,CAAC,CAuDrB"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Workerd-safe memoization for module-scope init promises.
3
+ *
4
+ * Nearly every SQL-backed store memoizes its table-creation promise at module
5
+ * scope (`let _initPromise`). On Cloudflare Workers (workerd) that pattern has
6
+ * a lethal failure mode: workerd cancels a request's pending I/O the moment
7
+ * its response returns, so an init promise CREATED during an early-responding
8
+ * request (auth probe, 404) FREEZES forever — and every later caller that
9
+ * awaits the memo hangs permanently. Proven live: `ensureObservabilityTables`
10
+ * frozen by a `get-session`-first ordering wedged every agent chat run at
11
+ * "Starting agent" on the unified Cloudflare runtime.
12
+ *
13
+ * `createInitMemo` keeps the single-flight memo semantics everywhere, and on
14
+ * workerd adds two layers of defense:
15
+ * 1. The init promise is tied to the creating request's lifetime via
16
+ * `__cf_ctx.waitUntil`, so workerd keeps its I/O alive to completion
17
+ * even when the response returns first (same remedy as the plugin-init
18
+ * freeze fix in framework-request-handler.ts).
19
+ * 2. Awaits on a still-pending memo are bounded; on timeout the memo is
20
+ * re-run under the CURRENT (live) request. Init bodies are idempotent
21
+ * (CREATE TABLE IF NOT EXISTS / guarded ALTERs), so a re-run is safe.
22
+ *
23
+ * On Node the behavior is identical to the raw memo pattern (single flight,
24
+ * failed init clears the memo so the next caller retries).
25
+ */
26
+ import { isCloudflareRuntime } from "./runtime.js";
27
+ /** How long a pending memo may be awaited on workerd before it is presumed
28
+ * frozen and re-run. Long enough for a slow cold init (Neon DDL over HTTP),
29
+ * short enough that a frozen memo degrades to a slow call, not a hang. */
30
+ export const INIT_MEMO_FROZEN_RETRY_MS = 15_000;
31
+ const FROZEN = Symbol("init-memo-frozen");
32
+ export function createInitMemo(init, options) {
33
+ const frozenRetryMs = options?.frozenRetryMs ?? INIT_MEMO_FROZEN_RETRY_MS;
34
+ let promise;
35
+ let settled = false;
36
+ const start = () => {
37
+ settled = false;
38
+ const p = init().then(() => {
39
+ settled = true;
40
+ }, (err) => {
41
+ // Failed init must not be memoized — the next caller retries.
42
+ promise = undefined;
43
+ throw err;
44
+ });
45
+ // Keep the init's I/O alive past the creating request's response on
46
+ // workerd. The extra catch keeps waitUntil from surfacing the rejection
47
+ // twice; callers still see it through the memoized promise.
48
+ try {
49
+ globalThis.__cf_ctx?.waitUntil?.(p.catch(() => { }));
50
+ }
51
+ catch {
52
+ /* not on Cloudflare — nothing to extend */
53
+ }
54
+ return p;
55
+ };
56
+ return async () => {
57
+ if (!promise)
58
+ promise = start();
59
+ if (settled || !isCloudflareRuntime())
60
+ return promise;
61
+ // workerd + still pending: the memo may belong to a completed request
62
+ // whose I/O was frozen. Bounded wait, then re-run under this request.
63
+ let timer;
64
+ const raced = await Promise.race([
65
+ promise,
66
+ new Promise((resolve) => {
67
+ timer = setTimeout(() => resolve(FROZEN), frozenRetryMs);
68
+ }),
69
+ ]).finally(() => {
70
+ if (timer)
71
+ clearTimeout(timer);
72
+ });
73
+ if (raced !== FROZEN)
74
+ return;
75
+ console.warn(`[agent-native] init memo${options?.label ? ` (${options.label})` : ""} still pending after ${frozenRetryMs}ms — presumed frozen by a completed request; re-running under the current request`);
76
+ promise = start();
77
+ return promise;
78
+ };
79
+ }
80
+ //# sourceMappingURL=init-memo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init-memo.js","sourceRoot":"","sources":["../../src/shared/init-memo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD;;0EAE0E;AAC1E,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEhD,MAAM,MAAM,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;AAE1C,MAAM,UAAU,cAAc,CAC5B,IAAyB,EACzB,OAAoD;IAEpD,MAAM,aAAa,GAAG,OAAO,EAAE,aAAa,IAAI,yBAAyB,CAAC;IAC1E,IAAI,OAAkC,CAAC;IACvC,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,MAAM,KAAK,GAAG,GAAkB,EAAE;QAChC,OAAO,GAAG,KAAK,CAAC;QAChB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CACnB,GAAG,EAAE;YACH,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;YACN,8DAA8D;YAC9D,OAAO,GAAG,SAAS,CAAC;YACpB,MAAM,GAAG,CAAC;QACZ,CAAC,CACF,CAAC;QACF,oEAAoE;QACpE,wEAAwE;QACxE,4DAA4D;QAC5D,IAAI,CAAC;YAED,UAGD,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,2CAA2C;QAC7C,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IAEF,OAAO,KAAK,IAAmB,EAAE;QAC/B,IAAI,CAAC,OAAO;YAAE,OAAO,GAAG,KAAK,EAAE,CAAC;QAChC,IAAI,OAAO,IAAI,CAAC,mBAAmB,EAAE;YAAE,OAAO,OAAO,CAAC;QAEtD,sEAAsE;QACtE,sEAAsE;QACtE,IAAI,KAAgD,CAAC;QACrD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAC/B,OAAO;YACP,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,EAAE;gBACrC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC;YAC3D,CAAC,CAAC;SACH,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;YACd,IAAI,KAAK;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QACH,IAAI,KAAK,KAAK,MAAM;YAAE,OAAO;QAE7B,OAAO,CAAC,IAAI,CACV,2BAA2B,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,wBAAwB,aAAa,mFAAmF,CAC/L,CAAC;QACF,OAAO,GAAG,KAAK,EAAE,CAAC;QAClB,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Workerd-safe memoization for module-scope init promises.\n *\n * Nearly every SQL-backed store memoizes its table-creation promise at module\n * scope (`let _initPromise`). On Cloudflare Workers (workerd) that pattern has\n * a lethal failure mode: workerd cancels a request's pending I/O the moment\n * its response returns, so an init promise CREATED during an early-responding\n * request (auth probe, 404) FREEZES forever — and every later caller that\n * awaits the memo hangs permanently. Proven live: `ensureObservabilityTables`\n * frozen by a `get-session`-first ordering wedged every agent chat run at\n * \"Starting agent\" on the unified Cloudflare runtime.\n *\n * `createInitMemo` keeps the single-flight memo semantics everywhere, and on\n * workerd adds two layers of defense:\n * 1. The init promise is tied to the creating request's lifetime via\n * `__cf_ctx.waitUntil`, so workerd keeps its I/O alive to completion\n * even when the response returns first (same remedy as the plugin-init\n * freeze fix in framework-request-handler.ts).\n * 2. Awaits on a still-pending memo are bounded; on timeout the memo is\n * re-run under the CURRENT (live) request. Init bodies are idempotent\n * (CREATE TABLE IF NOT EXISTS / guarded ALTERs), so a re-run is safe.\n *\n * On Node the behavior is identical to the raw memo pattern (single flight,\n * failed init clears the memo so the next caller retries).\n */\n\nimport { isCloudflareRuntime } from \"./runtime.js\";\n\n/** How long a pending memo may be awaited on workerd before it is presumed\n * frozen and re-run. Long enough for a slow cold init (Neon DDL over HTTP),\n * short enough that a frozen memo degrades to a slow call, not a hang. */\nexport const INIT_MEMO_FROZEN_RETRY_MS = 15_000;\n\nconst FROZEN = Symbol(\"init-memo-frozen\");\n\nexport function createInitMemo(\n init: () => Promise<void>,\n options?: { frozenRetryMs?: number; label?: string },\n): () => Promise<void> {\n const frozenRetryMs = options?.frozenRetryMs ?? INIT_MEMO_FROZEN_RETRY_MS;\n let promise: Promise<void> | undefined;\n let settled = false;\n\n const start = (): Promise<void> => {\n settled = false;\n const p = init().then(\n () => {\n settled = true;\n },\n (err) => {\n // Failed init must not be memoized — the next caller retries.\n promise = undefined;\n throw err;\n },\n );\n // Keep the init's I/O alive past the creating request's response on\n // workerd. The extra catch keeps waitUntil from surfacing the rejection\n // twice; callers still see it through the memoized promise.\n try {\n (\n globalThis as {\n __cf_ctx?: { waitUntil?: (p: Promise<unknown>) => void };\n }\n ).__cf_ctx?.waitUntil?.(p.catch(() => {}));\n } catch {\n /* not on Cloudflare — nothing to extend */\n }\n return p;\n };\n\n return async (): Promise<void> => {\n if (!promise) promise = start();\n if (settled || !isCloudflareRuntime()) return promise;\n\n // workerd + still pending: the memo may belong to a completed request\n // whose I/O was frozen. Bounded wait, then re-run under this request.\n let timer: ReturnType<typeof setTimeout> | undefined;\n const raced = await Promise.race([\n promise,\n new Promise<typeof FROZEN>((resolve) => {\n timer = setTimeout(() => resolve(FROZEN), frozenRetryMs);\n }),\n ]).finally(() => {\n if (timer) clearTimeout(timer);\n });\n if (raced !== FROZEN) return;\n\n console.warn(\n `[agent-native] init memo${options?.label ? ` (${options.label})` : \"\"} still pending after ${frozenRetryMs}ms — presumed frozen by a completed request; re-running under the current request`,\n );\n promise = start();\n return promise;\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,6BAA6B;IAC5C,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,aAAa,EAAE,GAAG,CAAC;IACnB,qDAAqD;IACrD,WAAW,EAAE,GAAG,CAAC;IACjB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,gBAAgB,CAAC,EACb,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,CAAC,CACC,QAAQ,EAAE,GAAG,EACb,GAAG,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAEzC,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,OAAO,CAAC,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC;IAChD;;;;;;;;;;OAUG;IACH,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK;QACtE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;OAMG;IACH,aAAa,CAAC,EACV,KAAK,GACL;QACE,uEAAuE;QACvE,YAAY,EAAE,MAAM,CAAC;QACrB,qEAAqE;QACrE,cAAc,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;QACtD,uEAAuE;QACvE,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;QACpD,iEAAiE;QACjE,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACP;AAuCD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,6BAA6B,GACnC,IAAI,CAGN;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,GACX,6BAA6B,GAAG,SAAS,CAE3C;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GACX,6BAA6B,CAS/B;AAED,wBAAgB,sBAAsB,IAAI,6BAA6B,EAAE,CAExE"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,MAAM,WAAW,6BAA6B;IAC5C,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,aAAa,EAAE,GAAG,CAAC;IACnB,qDAAqD;IACrD,WAAW,EAAE,GAAG,CAAC;IACjB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,gBAAgB,CAAC,EACb,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,CAAC,CACC,QAAQ,EAAE,GAAG,EACb,GAAG,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAEzC,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,OAAO,CAAC,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC;IAChD;;;;;;;;;;OAUG;IACH,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK;QACtE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;OAMG;IACH,aAAa,CAAC,EACV,KAAK,GACL;QACE,uEAAuE;QACvE,YAAY,EAAE,MAAM,CAAC;QACrB,qEAAqE;QACrE,cAAc,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;QACtD,uEAAuE;QACvE,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;QACpD,iEAAiE;QACjE,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACP;AAoCD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,6BAA6B,GACnC,IAAI,CAGN;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,GACX,6BAA6B,GAAG,SAAS,CAE3C;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GACX,6BAA6B,CAS/B;AAED,wBAAgB,sBAAsB,IAAI,6BAA6B,EAAE,CAExE"}
@@ -16,16 +16,7 @@
16
16
  * titleColumn: "title",
17
17
  * });
18
18
  */
19
- // Stash the registry on globalThis so it survives SSR bundle duplication.
20
- // Vite SSR's `noExternal: /^(?!node:)/` policy means @agent-native/core gets
21
- // inlined into every server bundle that imports it — and each bundle gets its
22
- // own module-level state. A plain `new Map()` here would create one Map per
23
- // bundle, so the template's `registerShareableResource()` (called from the
24
- // Nitro plugin graph) wouldn't be visible to the framework's auto-mounted
25
- // share-resource action (loaded via `import("../sharing/actions/...js")` in a
26
- // different module instance). Using globalThis collapses them back to one Map.
27
- const REGISTRY_KEY = "__agentNativeShareableResources__";
28
- const globalRegistry = globalThis;
19
+ import { getScopedGlobal } from "../shared/global-scope.js";
29
20
  function isTestRuntime() {
30
21
  return (process.env.NODE_ENV === "test" ||
31
22
  process.env.VITEST === "true" ||
@@ -36,12 +27,7 @@ function registrationCameFromTestFile() {
36
27
  return /[./\\](?:[^/\\]+[.-])(?:test|spec)\.[cm]?[jt]sx?(?::\d+)?(?::\d+)?/.test(stack);
37
28
  }
38
29
  function getRegistry() {
39
- let r = globalRegistry[REGISTRY_KEY];
40
- if (!r) {
41
- r = new Map();
42
- globalRegistry[REGISTRY_KEY] = r;
43
- }
44
- return r;
30
+ return getScopedGlobal("agent-native.sharing.shareable-resources", () => new Map());
45
31
  }
46
32
  export function registerShareableResource(entry) {
47
33
  if (!isTestRuntime() && registrationCameFromTestFile())
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA+GH,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,2EAA2E;AAC3E,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,MAAM,cAAc,GAClB,UAAiB,CAAC;AAEpB,SAAS,aAAa;IACpB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM;QAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM;QAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CAC3B,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;IACtC,OAAO,oEAAoE,CAAC,IAAI,CAC9E,KAAK,CACN,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,CAAC,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,IAAI,GAAG,EAAyC,CAAC;QACrD,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAoC;IAEpC,IAAI,CAAC,aAAa,EAAE,IAAI,4BAA4B,EAAE;QAAE,OAAO;IAC/D,WAAW,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAY;IAEZ,OAAO,WAAW,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,IAAY;IAEZ,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qCAAqC,IAAI,gDAAgD,CAC1F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\r\n * Registry of shareable resources.\r\n *\r\n * Each template registers its ownable resource(s) once on module load so the\r\n * framework-level share actions (`share-resource`, `list-resource-shares`,\r\n * etc.) can dispatch to the correct tables.\r\n *\r\n * import { registerShareableResource } from \"@agent-native/core/sharing\";\r\n * import * as schema from \"./schema.js\";\r\n *\r\n * registerShareableResource({\r\n * type: \"document\",\r\n * resourceTable: schema.documents,\r\n * sharesTable: schema.documentShares,\r\n * displayName: \"Document\",\r\n * titleColumn: \"title\",\r\n * });\r\n */\r\n\r\nexport interface ShareableResourceRegistration {\r\n /** Stable identifier used across actions, UI, and analytics. e.g. \"document\". */\r\n type: string;\r\n /** Drizzle table for the parent resource (must have ownableColumns()). */\r\n resourceTable: any;\r\n /** Drizzle table produced by createSharesTable(). */\r\n sharesTable: any;\r\n /** Human-readable singular label shown in the share dialog. */\r\n displayName: string;\r\n /**\r\n * Column on the resource table that holds a human-readable title for\r\n * display in the share UI. Default: \"title\".\r\n */\r\n titleColumn?: string;\r\n /**\r\n * Optional app-relative path to this resource. Used by share notifications\r\n * when the caller does not pass a more specific resourceUrl.\r\n */\r\n getResourcePath?: (resource: any) => string | undefined;\r\n /**\r\n * Drizzle DB accessor from the template's server/db/index.ts. Required —\r\n * the framework-level share actions and access helpers call this to reach\r\n * the right DB instance (schema is template-specific).\r\n */\r\n getDb: () => any;\r\n /**\r\n * When `false`, `visibility: \"public\"` is rejected by `set-resource-visibility`,\r\n * and `accessFilter` / `resolveAccess` treat any stored public row as private\r\n * (defense in depth — only owner + explicit shares grant access).\r\n *\r\n * Use this for resources that execute code or expose privileged data and must\r\n * never be reachable by a random authenticated user. Extensions set this:\r\n * extension HTML runs inside an iframe that calls actions / DB / proxied APIs\r\n * as the *viewer*, so a public extension would be arbitrary code with the\r\n * viewer's credentials.\r\n *\r\n * Default: `true` (matches the historical behavior — most resources can be public).\r\n */\r\n allowPublic?: boolean;\r\n /**\r\n * Optional role granted by a public-by-link read. Most resources should omit\r\n * this so public visibility remains viewer-only. Use narrowly for local or\r\n * otherwise constrained resources where the resource owner intentionally wants\r\n * unauthenticated link holders to do more than view.\r\n */\r\n publicAccessRole?:\r\n | \"viewer\"\r\n | \"editor\"\r\n | \"admin\"\r\n | ((\r\n resource: any,\r\n ctx: { userEmail?: string; orgId?: string },\r\n ) =>\r\n | \"viewer\"\r\n | \"editor\"\r\n | \"admin\"\r\n | Promise<\"viewer\" | \"editor\" | \"admin\">);\r\n /**\r\n * When `true`, individual user shares (`principalType: \"user\"`) must target\r\n * an email that is already a member of the same org as the resource, OR has\r\n * a pending invitation to that org. Cross-org user shares are rejected.\r\n *\r\n * Pair with `allowPublic: false` for resources that need a hard \"this org\r\n * only\" trust boundary. Extensions set this so a malicious caller can't\r\n * widen reach by sharing a code-executing extension to an outsider email.\r\n *\r\n * Default: `false` (matches the historical behavior — any email can be granted).\r\n */\r\n requireOrgMemberForUserShares?: boolean;\r\n /**\r\n * Optional per-resource access-context adapter. Most resources should use the\r\n * request user/org unchanged. Templates with an intentional alternate local\r\n * identity can normalize here so the generic framework sharing actions and\r\n * access helpers stay in sync with template-owned actions.\r\n */\r\n resolveAccessContext?: (ctx: { userEmail?: string; orgId?: string }) => {\r\n userEmail?: string;\r\n orgId?: string;\r\n };\r\n /**\r\n * When true, direct ownership is recognized by owner_email regardless of the\r\n * caller's active org. Use this only for resource types where the template's\r\n * own actions already treat owner_email as the cross-org authority and list\r\n * views add their own org filters.\r\n *\r\n * Default: `false`.\r\n */\r\n ownerAccessIgnoresOrg?: boolean;\r\n /**\r\n * Optional external-agent read handoff. When set, the framework-level\r\n * `create-agent-resource-link` action can mint a short-lived, read-only\r\n * `agent_access` URL for this resource. The context endpoint is owned by the\r\n * template so it can expose the same intentionally shareable shape as the\r\n * public page, not a generic raw database row.\r\n */\r\n agentReadable?:\r\n | false\r\n | {\r\n /** Token scope. Include the app name to avoid cross-app collisions. */\r\n resourceKind: string;\r\n /** App-relative JSON endpoint that accepts `id` + `agent_access`. */\r\n getContextPath: (resource: any) => string | undefined;\r\n /** Optional override for the page URL. Defaults to getResourcePath. */\r\n getPagePath?: (resource: any) => string | undefined;\r\n /** Optional override for the default two-hour token lifetime. */\r\n ttlSeconds?: number;\r\n };\r\n}\r\n\r\n// Stash the registry on globalThis so it survives SSR bundle duplication.\r\n// Vite SSR's `noExternal: /^(?!node:)/` policy means @agent-native/core gets\r\n// inlined into every server bundle that imports it — and each bundle gets its\r\n// own module-level state. A plain `new Map()` here would create one Map per\r\n// bundle, so the template's `registerShareableResource()` (called from the\r\n// Nitro plugin graph) wouldn't be visible to the framework's auto-mounted\r\n// share-resource action (loaded via `import(\"../sharing/actions/...js\")` in a\r\n// different module instance). Using globalThis collapses them back to one Map.\r\nconst REGISTRY_KEY = \"__agentNativeShareableResources__\";\r\ntype RegistryStore = Map<string, ShareableResourceRegistration>;\r\nconst globalRegistry: { [K in typeof REGISTRY_KEY]?: RegistryStore } =\r\n globalThis as any;\r\n\r\nfunction isTestRuntime(): boolean {\r\n return (\r\n process.env.NODE_ENV === \"test\" ||\r\n process.env.VITEST === \"true\" ||\r\n process.env.VITEST === \"1\"\r\n );\r\n}\r\n\r\nfunction registrationCameFromTestFile(): boolean {\r\n const stack = new Error().stack ?? \"\";\r\n return /[./\\\\](?:[^/\\\\]+[.-])(?:test|spec)\\.[cm]?[jt]sx?(?::\\d+)?(?::\\d+)?/.test(\r\n stack,\r\n );\r\n}\r\n\r\nfunction getRegistry(): RegistryStore {\r\n let r = globalRegistry[REGISTRY_KEY];\r\n if (!r) {\r\n r = new Map<string, ShareableResourceRegistration>();\r\n globalRegistry[REGISTRY_KEY] = r;\r\n }\r\n return r;\r\n}\r\n\r\nexport function registerShareableResource(\r\n entry: ShareableResourceRegistration,\r\n): void {\r\n if (!isTestRuntime() && registrationCameFromTestFile()) return;\r\n getRegistry().set(entry.type, entry);\r\n}\r\n\r\nexport function getShareableResource(\r\n type: string,\r\n): ShareableResourceRegistration | undefined {\r\n return getRegistry().get(type);\r\n}\r\n\r\nexport function requireShareableResource(\r\n type: string,\r\n): ShareableResourceRegistration {\r\n const reg = getRegistry();\r\n const entry = reg.get(type);\r\n if (!entry) {\r\n throw new Error(\r\n `Unknown shareable resource type: \"${type}\". Did you forget registerShareableResource()?`,\r\n );\r\n }\r\n return entry;\r\n}\r\n\r\nexport function listShareableResources(): ShareableResourceRegistration[] {\r\n return Array.from(getRegistry().values());\r\n}\r\n"]}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA2H5D,SAAS,aAAa;IACpB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM;QAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM;QAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CAC3B,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;IACtC,OAAO,oEAAoE,CAAC,IAAI,CAC9E,KAAK,CACN,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,eAAe,CACpB,0CAA0C,EAC1C,GAAG,EAAE,CAAC,IAAI,GAAG,EAAyC,CACvD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAoC;IAEpC,IAAI,CAAC,aAAa,EAAE,IAAI,4BAA4B,EAAE;QAAE,OAAO;IAC/D,WAAW,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAY;IAEZ,OAAO,WAAW,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,IAAY;IAEZ,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qCAAqC,IAAI,gDAAgD,CAC1F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * Registry of shareable resources.\n *\n * Each template registers its ownable resource(s) once on module load so the\n * framework-level share actions (`share-resource`, `list-resource-shares`,\n * etc.) can dispatch to the correct tables.\n *\n * import { registerShareableResource } from \"@agent-native/core/sharing\";\n * import * as schema from \"./schema.js\";\n *\n * registerShareableResource({\n * type: \"document\",\n * resourceTable: schema.documents,\n * sharesTable: schema.documentShares,\n * displayName: \"Document\",\n * titleColumn: \"title\",\n * });\n */\n\nimport { getScopedGlobal } from \"../shared/global-scope.js\";\n\nexport interface ShareableResourceRegistration {\n /** Stable identifier used across actions, UI, and analytics. e.g. \"document\". */\n type: string;\n /** Drizzle table for the parent resource (must have ownableColumns()). */\n resourceTable: any;\n /** Drizzle table produced by createSharesTable(). */\n sharesTable: any;\n /** Human-readable singular label shown in the share dialog. */\n displayName: string;\n /**\n * Column on the resource table that holds a human-readable title for\n * display in the share UI. Default: \"title\".\n */\n titleColumn?: string;\n /**\n * Optional app-relative path to this resource. Used by share notifications\n * when the caller does not pass a more specific resourceUrl.\n */\n getResourcePath?: (resource: any) => string | undefined;\n /**\n * Drizzle DB accessor from the template's server/db/index.ts. Required —\n * the framework-level share actions and access helpers call this to reach\n * the right DB instance (schema is template-specific).\n */\n getDb: () => any;\n /**\n * When `false`, `visibility: \"public\"` is rejected by `set-resource-visibility`,\n * and `accessFilter` / `resolveAccess` treat any stored public row as private\n * (defense in depth — only owner + explicit shares grant access).\n *\n * Use this for resources that execute code or expose privileged data and must\n * never be reachable by a random authenticated user. Extensions set this:\n * extension HTML runs inside an iframe that calls actions / DB / proxied APIs\n * as the *viewer*, so a public extension would be arbitrary code with the\n * viewer's credentials.\n *\n * Default: `true` (matches the historical behavior — most resources can be public).\n */\n allowPublic?: boolean;\n /**\n * Optional role granted by a public-by-link read. Most resources should omit\n * this so public visibility remains viewer-only. Use narrowly for local or\n * otherwise constrained resources where the resource owner intentionally wants\n * unauthenticated link holders to do more than view.\n */\n publicAccessRole?:\n | \"viewer\"\n | \"editor\"\n | \"admin\"\n | ((\n resource: any,\n ctx: { userEmail?: string; orgId?: string },\n ) =>\n | \"viewer\"\n | \"editor\"\n | \"admin\"\n | Promise<\"viewer\" | \"editor\" | \"admin\">);\n /**\n * When `true`, individual user shares (`principalType: \"user\"`) must target\n * an email that is already a member of the same org as the resource, OR has\n * a pending invitation to that org. Cross-org user shares are rejected.\n *\n * Pair with `allowPublic: false` for resources that need a hard \"this org\n * only\" trust boundary. Extensions set this so a malicious caller can't\n * widen reach by sharing a code-executing extension to an outsider email.\n *\n * Default: `false` (matches the historical behavior — any email can be granted).\n */\n requireOrgMemberForUserShares?: boolean;\n /**\n * Optional per-resource access-context adapter. Most resources should use the\n * request user/org unchanged. Templates with an intentional alternate local\n * identity can normalize here so the generic framework sharing actions and\n * access helpers stay in sync with template-owned actions.\n */\n resolveAccessContext?: (ctx: { userEmail?: string; orgId?: string }) => {\n userEmail?: string;\n orgId?: string;\n };\n /**\n * When true, direct ownership is recognized by owner_email regardless of the\n * caller's active org. Use this only for resource types where the template's\n * own actions already treat owner_email as the cross-org authority and list\n * views add their own org filters.\n *\n * Default: `false`.\n */\n ownerAccessIgnoresOrg?: boolean;\n /**\n * Optional external-agent read handoff. When set, the framework-level\n * `create-agent-resource-link` action can mint a short-lived, read-only\n * `agent_access` URL for this resource. The context endpoint is owned by the\n * template so it can expose the same intentionally shareable shape as the\n * public page, not a generic raw database row.\n */\n agentReadable?:\n | false\n | {\n /** Token scope. Include the app name to avoid cross-app collisions. */\n resourceKind: string;\n /** App-relative JSON endpoint that accepts `id` + `agent_access`. */\n getContextPath: (resource: any) => string | undefined;\n /** Optional override for the page URL. Defaults to getResourcePath. */\n getPagePath?: (resource: any) => string | undefined;\n /** Optional override for the default two-hour token lifetime. */\n ttlSeconds?: number;\n };\n}\n\n// Stash the registry on globalThis so it survives SSR bundle duplication.\n// Vite SSR's `noExternal: /^(?!node:)/` policy means @agent-native/core gets\n// inlined into every server bundle that imports it — and each bundle gets its\n// own module-level state. A plain `new Map()` here would create one Map per\n// bundle, so the template's `registerShareableResource()` (called from the\n// Nitro plugin graph) wouldn't be visible to the framework's auto-mounted\n// share-resource action (loaded via `import(\"../sharing/actions/...js\")` in a\n// different module instance). Using globalThis collapses them back to one Map.\n// Scope-aware + lazily resolved so unified workspace deployments (all apps in\n// one isolate) keep per-app registrations. See shared/global-scope.\ntype RegistryStore = Map<string, ShareableResourceRegistration>;\n\nfunction isTestRuntime(): boolean {\n return (\n process.env.NODE_ENV === \"test\" ||\n process.env.VITEST === \"true\" ||\n process.env.VITEST === \"1\"\n );\n}\n\nfunction registrationCameFromTestFile(): boolean {\n const stack = new Error().stack ?? \"\";\n return /[./\\\\](?:[^/\\\\]+[.-])(?:test|spec)\\.[cm]?[jt]sx?(?::\\d+)?(?::\\d+)?/.test(\n stack,\n );\n}\n\nfunction getRegistry(): RegistryStore {\n return getScopedGlobal(\n \"agent-native.sharing.shareable-resources\",\n () => new Map<string, ShareableResourceRegistration>(),\n );\n}\n\nexport function registerShareableResource(\n entry: ShareableResourceRegistration,\n): void {\n if (!isTestRuntime() && registrationCameFromTestFile()) return;\n getRegistry().set(entry.type, entry);\n}\n\nexport function getShareableResource(\n type: string,\n): ShareableResourceRegistration | undefined {\n return getRegistry().get(type);\n}\n\nexport function requireShareableResource(\n type: string,\n): ShareableResourceRegistration {\n const reg = getRegistry();\n const entry = reg.get(type);\n if (!entry) {\n throw new Error(\n `Unknown shareable resource type: \"${type}\". Did you forget registerShareableResource()?`,\n );\n }\n return entry;\n}\n\nexport function listShareableResources(): ShareableResourceRegistration[] {\n return Array.from(getRegistry().values());\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/tracking/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAyWH,wBAAgB,wBAAwB,IAAI,IAAI,CA+C/C"}
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/tracking/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA4WH,wBAAgB,wBAAwB,IAAI,IAAI,CA+C/C"}
@@ -12,6 +12,7 @@
12
12
  * Call `registerBuiltinProviders()` at server startup (done
13
13
  * automatically by the core-routes plugin).
14
14
  */
15
+ import { getScopedGlobal } from "../shared/global-scope.js";
15
16
  import { registerTrackingProvider } from "./registry.js";
16
17
  const POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com";
17
18
  const AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT = "https://analytics.jami.studio/track";
@@ -19,20 +20,21 @@ const BATCH_INTERVAL_MS = 10_000;
19
20
  const MAX_BATCH_SIZE = 50;
20
21
  // Use globalThis so multiple ESM graph instances (Vite dev + Nitro symlinks)
21
22
  // share one queue, matching the same pattern as the tracking registry.
22
- const QUEUE_KEY = Symbol.for("@agent-native/core/tracking.queue");
23
- const TIMER_KEY = Symbol.for("@agent-native/core/tracking.timer");
23
+ // Scope-aware + lazily resolved so unified workspace deployments keep
24
+ // per-app queues/timers. See shared/global-scope.
25
+ const QUEUE_BASE_KEY = "agent-native.tracking.queue";
26
+ const TIMER_BASE_KEY = "agent-native.tracking.timer";
24
27
  function getQueue() {
25
- const g = globalThis;
26
- if (!g[QUEUE_KEY])
27
- g[QUEUE_KEY] = [];
28
- return g[QUEUE_KEY];
28
+ return getScopedGlobal(QUEUE_BASE_KEY, () => []);
29
+ }
30
+ function getTimerBox() {
31
+ return getScopedGlobal(TIMER_BASE_KEY, () => ({ timer: null }));
29
32
  }
30
33
  function getTimer() {
31
- const g = globalThis;
32
- return g[TIMER_KEY] ?? null;
34
+ return getTimerBox().timer;
33
35
  }
34
36
  function setTimer(t) {
35
- globalThis[TIMER_KEY] = t;
37
+ getTimerBox().timer = t;
36
38
  }
37
39
  function enqueue(url, body, headers, options) {
38
40
  const queue = getQueue();