@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,129 +1,132 @@
1
- /**
2
- * In-process registry of required / optional secrets.
3
- *
4
- * Templates call `registerRequiredSecret()` at module load time — typically
5
- * from a server plugin. The secrets HTTP routes and the sidebar settings UI
6
- * read from this registry on every request so overrides and late-registered
7
- * secrets are picked up without a restart.
8
- */
9
-
10
- export type SecretScope = "user" | "workspace" | "org";
11
- export type SecretKind = "api-key" | "oauth";
12
-
13
- export interface ValidatorResult {
14
- ok: boolean;
15
- error?: string;
16
- }
17
-
18
- export interface SecretValidator {
19
- (
20
- value: string,
21
- ): Promise<ValidatorResult | boolean> | ValidatorResult | boolean;
22
- }
23
-
24
- export interface RegisteredSecret {
25
- /** Env var name & settings key — e.g. "OPENAI_API_KEY". */
26
- key: string;
27
- /** Human-readable label shown in the sidebar. */
28
- label: string;
29
- /** Short description shown below the label. */
30
- description?: string;
31
- /** URL where the user can obtain the key or connect the account. */
32
- docsUrl?: string;
33
- /** Whether the secret is per-user or shared across a workspace/org. */
34
- scope: SecretScope;
35
- /** UI affordance: "api-key" renders an input; "oauth" renders Connect. */
36
- kind: SecretKind;
37
- /** When true, an onboarding step is auto-injected for this secret. */
38
- required?: boolean;
39
- /**
40
- * Optional health check. Receives the plain-text value, returns `true` or
41
- * `{ ok: true }` on success. Returning `{ ok: false, error }` surfaces the
42
- * error to the UI. Never log the value from inside the validator.
43
- */
44
- validator?: SecretValidator;
45
- /**
46
- * For `kind: "oauth"` — the oauth-tokens provider id (e.g. "google") that
47
- * backs this registration. Used to surface OAuth status in the unified UI.
48
- */
49
- oauthProvider?: string;
50
- /**
51
- * For `kind: "oauth"` — URL the Connect button should point at. Typically
52
- * the framework's `/_agent-native/google/auth-url` or similar.
53
- */
54
- oauthConnectUrl?: string;
55
- }
56
-
57
- // Pin the registry to globalThis so templates that load `@agent-native/core`
58
- // via more than one ESM graph (e.g. dev-mode Vite + Nitro, symlinked
59
- // node_modules, dist/ vs src/) share a single registry. Without this, a
60
- // template's `register-secrets.ts` side-effect module may populate one
61
- // registry instance while the /_agent-native/secrets route reads from
62
- // another net effect: the UI sees an empty list.
63
- const REGISTRY_KEY = Symbol.for("@agent-native/core/secrets.registry");
64
- interface GlobalWithRegistry {
65
- [REGISTRY_KEY]?: Map<string, RegisteredSecret>;
66
- }
67
- const registry: Map<string, RegisteredSecret> = ((
68
- globalThis as unknown as GlobalWithRegistry
69
- )[REGISTRY_KEY] ??= new Map());
70
-
71
- /**
72
- * Register (or override) a required secret.
73
- *
74
- * Subsequent registrations with the same `key` replace the previous
75
- * definition later plugins can override framework defaults.
76
- */
77
- export function registerRequiredSecret(secret: RegisteredSecret): void {
78
- if (!secret || typeof secret.key !== "string" || !secret.key) {
79
- throw new Error("registerRequiredSecret: secret.key is required");
80
- }
81
- if (
82
- secret.scope !== "user" &&
83
- secret.scope !== "workspace" &&
84
- secret.scope !== "org"
85
- ) {
86
- throw new Error(
87
- `registerRequiredSecret: secret.scope must be "user", "workspace", or "org" (got "${secret.scope}")`,
88
- );
89
- }
90
- if (secret.kind !== "api-key" && secret.kind !== "oauth") {
91
- throw new Error(
92
- `registerRequiredSecret: secret.kind must be "api-key" or "oauth" (got "${secret.kind}")`,
93
- );
94
- }
95
- if (registry.has(secret.key) && process.env.DEBUG) {
96
- console.log(
97
- `[agent-native] Overriding registered secret "${secret.key}" with new registration.`,
98
- );
99
- }
100
- registry.set(secret.key, secret);
101
-
102
- // Auto-inject an onboarding step for required secrets. Done via dynamic
103
- // import to avoid a load-order cycle between register and the onboarding
104
- // registry during module bootstrap.
105
- if (secret.required) {
106
- // Lazy import resolved synchronously in practice because the module is
107
- // already loaded once any route handler runs, but tolerate async.
108
- import("./onboarding.js")
109
- .then((mod) => mod.maybeRegisterSecretOnboardingStep(secret))
110
- .catch(() => {
111
- // Onboarding is optional — never let it block registration.
112
- });
113
- }
114
- }
115
-
116
- /** Return all registered secrets in registration order. */
117
- export function listRequiredSecrets(): RegisteredSecret[] {
118
- return Array.from(registry.values());
119
- }
120
-
121
- /** Look up a single registered secret by key. */
122
- export function getRequiredSecret(key: string): RegisteredSecret | undefined {
123
- return registry.get(key);
124
- }
125
-
126
- /** Test helper — clears the registry between runs. */
127
- export function __resetSecretsRegistry(): void {
128
- registry.clear();
129
- }
1
+ /**
2
+ * In-process registry of required / optional secrets.
3
+ *
4
+ * Templates call `registerRequiredSecret()` at module load time — typically
5
+ * from a server plugin. The secrets HTTP routes and the sidebar settings UI
6
+ * read from this registry on every request so overrides and late-registered
7
+ * secrets are picked up without a restart.
8
+ */
9
+
10
+ import { getScopedGlobal } from "../shared/global-scope.js";
11
+
12
+ export type SecretScope = "user" | "workspace" | "org";
13
+ export type SecretKind = "api-key" | "oauth";
14
+
15
+ export interface ValidatorResult {
16
+ ok: boolean;
17
+ error?: string;
18
+ }
19
+
20
+ export interface SecretValidator {
21
+ (
22
+ value: string,
23
+ ): Promise<ValidatorResult | boolean> | ValidatorResult | boolean;
24
+ }
25
+
26
+ export interface RegisteredSecret {
27
+ /** Env var name & settings key — e.g. "OPENAI_API_KEY". */
28
+ key: string;
29
+ /** Human-readable label shown in the sidebar. */
30
+ label: string;
31
+ /** Short description shown below the label. */
32
+ description?: string;
33
+ /** URL where the user can obtain the key or connect the account. */
34
+ docsUrl?: string;
35
+ /** Whether the secret is per-user or shared across a workspace/org. */
36
+ scope: SecretScope;
37
+ /** UI affordance: "api-key" renders an input; "oauth" renders Connect. */
38
+ kind: SecretKind;
39
+ /** When true, an onboarding step is auto-injected for this secret. */
40
+ required?: boolean;
41
+ /**
42
+ * Optional health check. Receives the plain-text value, returns `true` or
43
+ * `{ ok: true }` on success. Returning `{ ok: false, error }` surfaces the
44
+ * error to the UI. Never log the value from inside the validator.
45
+ */
46
+ validator?: SecretValidator;
47
+ /**
48
+ * For `kind: "oauth"` — the oauth-tokens provider id (e.g. "google") that
49
+ * backs this registration. Used to surface OAuth status in the unified UI.
50
+ */
51
+ oauthProvider?: string;
52
+ /**
53
+ * For `kind: "oauth"` — URL the Connect button should point at. Typically
54
+ * the framework's `/_agent-native/google/auth-url` or similar.
55
+ */
56
+ oauthConnectUrl?: string;
57
+ }
58
+
59
+ // Pin the registry to globalThis so templates that load `@agent-native/core`
60
+ // via more than one ESM graph (e.g. dev-mode Vite + Nitro, symlinked
61
+ // node_modules, dist/ vs src/) share a single registry. Without this, a
62
+ // template's `register-secrets.ts` side-effect module may populate one
63
+ // registry instance while the /_agent-native/secrets route reads from
64
+ // another — net effect: the UI sees an empty list.
65
+ // Scope-aware + lazily resolved so unified workspace deployments (all apps in
66
+ // one isolate) keep per-app secret registrations. See shared/global-scope.
67
+ function getSecretsRegistry(): Map<string, RegisteredSecret> {
68
+ return getScopedGlobal(
69
+ "agent-native.secrets.registry",
70
+ () => new Map<string, RegisteredSecret>(),
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Register (or override) a required secret.
76
+ *
77
+ * Subsequent registrations with the same `key` replace the previous
78
+ * definition later plugins can override framework defaults.
79
+ */
80
+ export function registerRequiredSecret(secret: RegisteredSecret): void {
81
+ if (!secret || typeof secret.key !== "string" || !secret.key) {
82
+ throw new Error("registerRequiredSecret: secret.key is required");
83
+ }
84
+ if (
85
+ secret.scope !== "user" &&
86
+ secret.scope !== "workspace" &&
87
+ secret.scope !== "org"
88
+ ) {
89
+ throw new Error(
90
+ `registerRequiredSecret: secret.scope must be "user", "workspace", or "org" (got "${secret.scope}")`,
91
+ );
92
+ }
93
+ if (secret.kind !== "api-key" && secret.kind !== "oauth") {
94
+ throw new Error(
95
+ `registerRequiredSecret: secret.kind must be "api-key" or "oauth" (got "${secret.kind}")`,
96
+ );
97
+ }
98
+ if (getSecretsRegistry().has(secret.key) && process.env.DEBUG) {
99
+ console.log(
100
+ `[agent-native] Overriding registered secret "${secret.key}" with new registration.`,
101
+ );
102
+ }
103
+ getSecretsRegistry().set(secret.key, secret);
104
+
105
+ // Auto-inject an onboarding step for required secrets. Done via dynamic
106
+ // import to avoid a load-order cycle between register and the onboarding
107
+ // registry during module bootstrap.
108
+ if (secret.required) {
109
+ // Lazy import — resolved synchronously in practice because the module is
110
+ // already loaded once any route handler runs, but tolerate async.
111
+ import("./onboarding.js")
112
+ .then((mod) => mod.maybeRegisterSecretOnboardingStep(secret))
113
+ .catch(() => {
114
+ // Onboarding is optional — never let it block registration.
115
+ });
116
+ }
117
+ }
118
+
119
+ /** Return all registered secrets in registration order. */
120
+ export function listRequiredSecrets(): RegisteredSecret[] {
121
+ return Array.from(getSecretsRegistry().values());
122
+ }
123
+
124
+ /** Look up a single registered secret by key. */
125
+ export function getRequiredSecret(key: string): RegisteredSecret | undefined {
126
+ return getSecretsRegistry().get(key);
127
+ }
128
+
129
+ /** Test helper — clears the registry between runs. */
130
+ export function __resetSecretsRegistry(): void {
131
+ getSecretsRegistry().clear();
132
+ }
@@ -0,0 +1,75 @@
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
+ let moduleGraphScopeId: string | null = null;
34
+
35
+ /**
36
+ * Set the global-registry scope for THIS module graph (this app's bundle).
37
+ * Called by the generated worker entry's scope-init module on unified
38
+ * workspace deployments. Pass `null` to clear (tests).
39
+ */
40
+ export function setGlobalScopeId(id: string | null): void {
41
+ const trimmed = typeof id === "string" ? id.trim() : "";
42
+ moduleGraphScopeId = trimmed ? trimmed : null;
43
+ }
44
+
45
+ /** The active scope id for this module graph, or `null` when unscoped. */
46
+ export function getGlobalScopeId(): string | null {
47
+ return moduleGraphScopeId;
48
+ }
49
+
50
+ /**
51
+ * The fully-qualified global key name for `base` under the active scope.
52
+ * Unscoped graphs get `base` unchanged, so existing dev-mode behavior
53
+ * (one registry across all of an app's ESM graphs) is preserved.
54
+ */
55
+ export function scopedGlobalKeyName(base: string): string {
56
+ return moduleGraphScopeId ? `${base}::app:${moduleGraphScopeId}` : base;
57
+ }
58
+
59
+ /**
60
+ * Lazily resolve (and initialize once) a globalThis-pinned singleton under
61
+ * the scope-aware key for `base`. Registries MUST call this per access —
62
+ * never capture the result in module scope — so a scope id set during
63
+ * entry-module evaluation is honored by every later registration and read.
64
+ */
65
+ export function getScopedGlobal<T>(base: string, init: () => T): T {
66
+ const key = Symbol.for(scopedGlobalKeyName(base));
67
+ const g = globalThis as unknown as Record<symbol, T | undefined>;
68
+ return (g[key] ??= init());
69
+ }
70
+
71
+ /** Test helper — delete the pinned singleton for `base` in the ACTIVE scope. */
72
+ export function __deleteScopedGlobal(base: string): void {
73
+ const key = Symbol.for(scopedGlobalKeyName(base));
74
+ delete (globalThis as unknown as Record<symbol, unknown>)[key];
75
+ }
@@ -0,0 +1,94 @@
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
+
27
+ import { isCloudflareRuntime } from "./runtime.js";
28
+
29
+ /** How long a pending memo may be awaited on workerd before it is presumed
30
+ * frozen and re-run. Long enough for a slow cold init (Neon DDL over HTTP),
31
+ * short enough that a frozen memo degrades to a slow call, not a hang. */
32
+ export const INIT_MEMO_FROZEN_RETRY_MS = 15_000;
33
+
34
+ const FROZEN = Symbol("init-memo-frozen");
35
+
36
+ export function createInitMemo(
37
+ init: () => Promise<void>,
38
+ options?: { frozenRetryMs?: number; label?: string },
39
+ ): () => Promise<void> {
40
+ const frozenRetryMs = options?.frozenRetryMs ?? INIT_MEMO_FROZEN_RETRY_MS;
41
+ let promise: Promise<void> | undefined;
42
+ let settled = false;
43
+
44
+ const start = (): Promise<void> => {
45
+ settled = false;
46
+ const p = init().then(
47
+ () => {
48
+ settled = true;
49
+ },
50
+ (err) => {
51
+ // Failed init must not be memoized — the next caller retries.
52
+ promise = undefined;
53
+ throw err;
54
+ },
55
+ );
56
+ // Keep the init's I/O alive past the creating request's response on
57
+ // workerd. The extra catch keeps waitUntil from surfacing the rejection
58
+ // twice; callers still see it through the memoized promise.
59
+ try {
60
+ (
61
+ globalThis as {
62
+ __cf_ctx?: { waitUntil?: (p: Promise<unknown>) => void };
63
+ }
64
+ ).__cf_ctx?.waitUntil?.(p.catch(() => {}));
65
+ } catch {
66
+ /* not on Cloudflare — nothing to extend */
67
+ }
68
+ return p;
69
+ };
70
+
71
+ return async (): Promise<void> => {
72
+ if (!promise) promise = start();
73
+ if (settled || !isCloudflareRuntime()) return promise;
74
+
75
+ // workerd + still pending: the memo may belong to a completed request
76
+ // whose I/O was frozen. Bounded wait, then re-run under this request.
77
+ let timer: ReturnType<typeof setTimeout> | undefined;
78
+ const raced = await Promise.race([
79
+ promise,
80
+ new Promise<typeof FROZEN>((resolve) => {
81
+ timer = setTimeout(() => resolve(FROZEN), frozenRetryMs);
82
+ }),
83
+ ]).finally(() => {
84
+ if (timer) clearTimeout(timer);
85
+ });
86
+ if (raced !== FROZEN) return;
87
+
88
+ console.warn(
89
+ `[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`,
90
+ );
91
+ promise = start();
92
+ return promise;
93
+ };
94
+ }