@noodleseed/one 0.170.0 → 0.171.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.
Files changed (105) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +3 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/catalog.d.ts.map +1 -1
  5. package/dist/commands/catalog.js +2 -1
  6. package/dist/commands/catalog.js.map +1 -1
  7. package/dist/commands/whatsapp-ops.d.ts +13 -0
  8. package/dist/commands/whatsapp-ops.d.ts.map +1 -0
  9. package/dist/commands/whatsapp-ops.js +244 -0
  10. package/dist/commands/whatsapp-ops.js.map +1 -0
  11. package/dist/commands/whatsapp-output.d.ts +3 -0
  12. package/dist/commands/whatsapp-output.d.ts.map +1 -0
  13. package/dist/commands/whatsapp-output.js +70 -0
  14. package/dist/commands/whatsapp-output.js.map +1 -0
  15. package/dist/dev.js +2 -0
  16. package/dist/dev.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/plugin-mode/plugin-operation-contract.d.ts +1 -1
  22. package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
  23. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +1 -0
  24. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +3 -0
  26. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +6 -0
  27. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  28. package/node_modules/@noodle-borg/app-audit/dist/audit-embedded-assistant.js +2 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +2 -1
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +9 -4
  31. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.d.ts +16 -0
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.js +19 -0
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +145 -0
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +378 -0
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +160 -0
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.d.ts +18 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.js +69 -0
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-spend.js +82 -0
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +42 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.js +76 -0
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +134 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +72 -0
  43. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +20 -0
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +221 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +15 -0
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +34 -0
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +19 -1
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +22 -6
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +7 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +7 -0
  51. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-appearance-store.js +83 -0
  52. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-elevation.js +151 -0
  53. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interaction-writes.js +45 -0
  54. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interactions.js +340 -0
  55. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant.js +439 -0
  56. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-channel-store.js +102 -0
  57. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-continuity-store.js +159 -0
  58. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-elevation-store.js +110 -0
  59. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-embed-store.js +134 -0
  60. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres.js +11 -0
  61. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +8 -0
  62. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +23 -0
  63. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +17 -2
  64. package/node_modules/@noodle-borg/authoring/dist/assistant.js +48 -19
  65. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  66. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
  67. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  68. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +121 -0
  69. package/node_modules/@noodle-borg/cli-catalog/dist/index.js +1 -0
  70. package/node_modules/@noodle-borg/compiler/dist/artifact/config.js +1 -1
  71. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  72. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +72 -12
  73. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +51 -27
  74. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +1 -0
  75. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +19 -0
  76. package/node_modules/@noodle-borg/module-audit/dist/index.js +37 -0
  77. package/node_modules/@noodle-borg/module-audit/dist/postgres-audit.js +126 -0
  78. package/node_modules/@noodle-borg/module-audit/dist/route.js +66 -0
  79. package/node_modules/@noodle-borg/runtime/dist/managed-origins.js +8 -4
  80. package/node_modules/@noodle-borg/runtime/dist/postgres-state-handles.js +311 -0
  81. package/node_modules/@noodle-borg/runtime/dist/postgres.js +4 -0
  82. package/node_modules/@noodle-borg/service/dist/assistant-composition.js +18 -0
  83. package/node_modules/@noodle-borg/service/dist/build-info.js +5 -2
  84. package/node_modules/@noodle-borg/service/dist/business-information/source-ingestion-coordinator.js +7 -0
  85. package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +233 -0
  86. package/node_modules/@noodle-borg/service/dist/channels/cipher.js +37 -0
  87. package/node_modules/@noodle-borg/service/dist/channels/composition.js +37 -0
  88. package/node_modules/@noodle-borg/service/dist/channels/inference-guard.js +53 -0
  89. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +337 -0
  90. package/node_modules/@noodle-borg/service/dist/channels/worker-loop.js +50 -0
  91. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +62 -34
  92. package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +6 -2
  93. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +14 -1
  94. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +238 -0
  95. package/node_modules/@noodle-borg/service/dist/service.js +30 -32
  96. package/node_modules/@noodle-borg/service/package.json +1 -1
  97. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +33 -0
  98. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +9 -0
  99. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  100. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  101. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +2 -0
  102. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +10 -2
  103. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +426 -0
  104. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +213 -0
  105. package/package.json +2 -2
@@ -0,0 +1,159 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { continuityDigest, continuityHandle, continuityMaxRestores, continuityWindowMs, } from './continuity-store.js';
3
+ function toRecord(row) {
4
+ return {
5
+ id: row.id,
6
+ sessionId: row.session_id,
7
+ tenant: { org: row.org_slug, app: row.app_slug, env: row.environment },
8
+ context: {
9
+ embedId: row.embed_id,
10
+ originHash: row.origin_hash,
11
+ visitorHash: row.visitor_hash,
12
+ },
13
+ handleHash: row.handle_hash,
14
+ restoreCount: row.restore_count,
15
+ maxRestores: row.max_restores,
16
+ createdAt: row.created_at.toISOString(),
17
+ expiresAt: row.expires_at.toISOString(),
18
+ ...(row.claimed_at === null ? {} : { claimedAt: row.claimed_at.toISOString() }),
19
+ };
20
+ }
21
+ /**
22
+ * Durable continuity handles (ADR 0223, clauses 11-16).
23
+ *
24
+ * Its own table, deliberately not a second row kind in `assistant_elevations`. A display-only handle and
25
+ * an identity-transition ticket must not share storage, so that a defect in either cannot reach the
26
+ * other and a reader never has to work out which rows are dangerous.
27
+ *
28
+ * Two properties are enforced by the database rather than by application order, exactly as they are for
29
+ * elevations. A **partial unique index over unclaimed rows** keeps at most one live handle per session,
30
+ * so each turn's handle supersedes rather than accumulating one live key per turn. And the claim is a
31
+ * **single statement** — `UPDATE … WHERE claimed_at IS NULL … RETURNING` — because two backends racing
32
+ * one handle is precisely the case a read-then-write loses, and the loser would restore a second time.
33
+ *
34
+ * The refusal reasons are distinguished by re-reading the row *without* consuming it: a wrong-context
35
+ * probe must not spend the rightful visitor's handle, or reading someone's conversation becomes
36
+ * unnecessary and denying it becomes trivial.
37
+ */
38
+ export class PostgresAssistantContinuityStore {
39
+ #pool;
40
+ constructor(pool) {
41
+ this.#pool = pool;
42
+ }
43
+ async ensureSchema() {
44
+ await this.#pool.query(`
45
+ CREATE TABLE IF NOT EXISTS assistant_continuity_handles (
46
+ id text PRIMARY KEY,
47
+ session_id text NOT NULL,
48
+ org_slug text NOT NULL,
49
+ app_slug text NOT NULL,
50
+ environment text NOT NULL,
51
+ embed_id text NOT NULL,
52
+ origin_hash text NOT NULL,
53
+ visitor_hash text NOT NULL,
54
+ handle_hash text NOT NULL,
55
+ restore_count integer NOT NULL DEFAULT 0,
56
+ max_restores integer NOT NULL,
57
+ created_at timestamptz NOT NULL DEFAULT now(),
58
+ expires_at timestamptz NOT NULL,
59
+ claimed_at timestamptz
60
+ )
61
+ `);
62
+ await this.#pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS assistant_continuity_handle_key
63
+ ON assistant_continuity_handles (handle_hash)`);
64
+ await this.#pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS assistant_continuity_live_session_key
65
+ ON assistant_continuity_handles (session_id) WHERE claimed_at IS NULL`);
66
+ // Retention is part of this store rather than an afterthought: handles are short-lived and must be
67
+ // swept, not accumulated, and the sweep needs this index to stay cheap as the table grows.
68
+ await this.#pool.query(`CREATE INDEX IF NOT EXISTS assistant_continuity_expires_at_idx
69
+ ON assistant_continuity_handles (expires_at)`);
70
+ }
71
+ async issue(input) {
72
+ return this.#write({ ...input, restoreCount: 0 });
73
+ }
74
+ async claim(input) {
75
+ const hash = continuityDigest(input.handle);
76
+ const { rows } = await this.#pool.query(`UPDATE assistant_continuity_handles
77
+ SET claimed_at = $2, restore_count = restore_count + 1
78
+ WHERE handle_hash = $1
79
+ AND claimed_at IS NULL
80
+ AND expires_at > $2
81
+ AND embed_id = $3 AND origin_hash = $4 AND visitor_hash = $5
82
+ RETURNING *`, [
83
+ hash,
84
+ input.now.toISOString(),
85
+ input.context.embedId,
86
+ input.context.originHash,
87
+ input.context.visitorHash,
88
+ ]);
89
+ const claimed = rows[0];
90
+ if (!claimed)
91
+ return this.#explainRefusal(hash, input.context);
92
+ const record = toRecord(claimed);
93
+ if (record.restoreCount >= record.maxRestores)
94
+ return { ok: true, record };
95
+ const rotated = await this.#write({
96
+ sessionId: record.sessionId,
97
+ tenant: record.tenant,
98
+ context: record.context,
99
+ // Re-derive from the record so a rotation can never widen what the first issue clamped.
100
+ windowMs: Date.parse(record.expiresAt) - Date.parse(record.createdAt),
101
+ maxRestores: record.maxRestores,
102
+ restoreCount: record.restoreCount,
103
+ now: input.now,
104
+ });
105
+ return rotated === undefined
106
+ ? { ok: true, record }
107
+ : { ok: true, record, handle: rotated.handle };
108
+ }
109
+ /** Remove spent and expired rows. Callers may lag expiry: `claim` already refuses a stale handle. */
110
+ async sweepExpired(input) {
111
+ const { rowCount } = await this.#pool.query('DELETE FROM assistant_continuity_handles WHERE expires_at <= $1 OR claimed_at IS NOT NULL', [input.now.toISOString()]);
112
+ return rowCount ?? 0;
113
+ }
114
+ async #explainRefusal(hash, context) {
115
+ const { rows } = await this.#pool.query('SELECT * FROM assistant_continuity_handles WHERE handle_hash = $1', [hash]);
116
+ const row = rows[0];
117
+ if (!row || row.claimed_at !== null)
118
+ return { ok: false, reason: 'unknown' };
119
+ if (row.embed_id !== context.embedId ||
120
+ row.origin_hash !== context.originHash ||
121
+ row.visitor_hash !== context.visitorHash) {
122
+ return { ok: false, reason: 'context_mismatch' };
123
+ }
124
+ return { ok: false, reason: 'expired' };
125
+ }
126
+ async #write(input) {
127
+ const windowMs = continuityWindowMs(input.windowMs);
128
+ const maxRestores = continuityMaxRestores(input.maxRestores);
129
+ if (windowMs === 0 || maxRestores === 0)
130
+ return undefined;
131
+ // Delete before insert so the partial unique index cannot reject the supersede.
132
+ await this.#pool.query('DELETE FROM assistant_continuity_handles WHERE session_id = $1 AND claimed_at IS NULL', [input.sessionId]);
133
+ const handle = continuityHandle();
134
+ const { rows } = await this.#pool.query(`INSERT INTO assistant_continuity_handles
135
+ (id, session_id, org_slug, app_slug, environment, embed_id, origin_hash, visitor_hash,
136
+ handle_hash, restore_count, max_restores, created_at, expires_at)
137
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
138
+ RETURNING *`, [
139
+ `cont_${randomUUID().replaceAll('-', '')}`,
140
+ input.sessionId,
141
+ input.tenant.org,
142
+ input.tenant.app,
143
+ input.tenant.env,
144
+ input.context.embedId,
145
+ input.context.originHash,
146
+ input.context.visitorHash,
147
+ continuityDigest(handle),
148
+ input.restoreCount,
149
+ maxRestores,
150
+ input.now.toISOString(),
151
+ new Date(input.now.getTime() + windowMs).toISOString(),
152
+ ]);
153
+ const row = rows[0];
154
+ if (!row)
155
+ throw new Error('continuity insert returned no row');
156
+ return { record: toRecord(row), handle };
157
+ }
158
+ }
159
+ //# sourceMappingURL=postgres-continuity-store.js.map
@@ -0,0 +1,110 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ASSISTANT_ELEVATION_TTL_MS, elevationContinuation, elevationDigest, } from './elevation-store.js';
3
+ function toRecord(row) {
4
+ return {
5
+ id: row.id,
6
+ sessionId: row.session_id,
7
+ tenant: { org: row.org_slug, app: row.app_slug, env: row.environment },
8
+ tool: row.tool,
9
+ claimableStateHandles: [...(row.claimable_state_handles ?? [])],
10
+ continuationHash: row.continuation_hash,
11
+ createdAt: row.created_at.toISOString(),
12
+ expiresAt: row.expires_at.toISOString(),
13
+ ...(row.claimed_at === null ? {} : { claimedAt: row.claimed_at.toISOString() }),
14
+ };
15
+ }
16
+ /**
17
+ * Durable elevation continuations.
18
+ *
19
+ * Two properties are load-bearing and both are enforced by the database rather than by application
20
+ * order. A **partial unique index over unclaimed rows** keeps at most one live elevation per session, so
21
+ * a model asking twice supersedes rather than accumulating keys to the same conversation. And the claim
22
+ * is a **single statement** — `UPDATE … WHERE claimed_at IS NULL … RETURNING` — because two backends
23
+ * racing the same continuation is exactly the case a read-then-write loses, and the loser would elevate
24
+ * a second time.
25
+ *
26
+ * The refusal reasons are distinguished by re-reading the row *without* consuming it: a tenant mismatch
27
+ * must not spend the rightful owner's continuation, or a probe becomes a denial of service.
28
+ */
29
+ export class PostgresAssistantElevationStore {
30
+ #pool;
31
+ constructor(pool) {
32
+ this.#pool = pool;
33
+ }
34
+ async ensureSchema() {
35
+ await this.#pool.query(`
36
+ CREATE TABLE IF NOT EXISTS assistant_elevations (
37
+ id text PRIMARY KEY,
38
+ session_id text NOT NULL,
39
+ org_slug text NOT NULL,
40
+ app_slug text NOT NULL,
41
+ environment text NOT NULL,
42
+ tool text NOT NULL,
43
+ claimable_state_handles jsonb NOT NULL DEFAULT '[]'::jsonb,
44
+ continuation_hash text NOT NULL,
45
+ created_at timestamptz NOT NULL DEFAULT now(),
46
+ expires_at timestamptz NOT NULL,
47
+ claimed_at timestamptz
48
+ )
49
+ `);
50
+ await this.#pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS assistant_elevations_continuation_key
51
+ ON assistant_elevations (continuation_hash)`);
52
+ await this.#pool.query(`
53
+ ALTER TABLE assistant_elevations
54
+ ADD COLUMN IF NOT EXISTS claimable_state_handles jsonb NOT NULL DEFAULT '[]'::jsonb
55
+ `);
56
+ await this.#pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS assistant_elevations_live_session_key
57
+ ON assistant_elevations (session_id) WHERE claimed_at IS NULL`);
58
+ }
59
+ async request(input) {
60
+ // Delete before insert so the partial unique index cannot reject the supersede.
61
+ await this.#pool.query('DELETE FROM assistant_elevations WHERE session_id = $1 AND claimed_at IS NULL', [input.sessionId]);
62
+ const continuation = elevationContinuation();
63
+ const { rows } = await this.#pool.query(`INSERT INTO assistant_elevations
64
+ (id, session_id, org_slug, app_slug, environment, tool, claimable_state_handles,
65
+ continuation_hash, created_at, expires_at)
66
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10)
67
+ RETURNING *`, [
68
+ `elev_${randomUUID().replaceAll('-', '')}`,
69
+ input.sessionId,
70
+ input.tenant.org,
71
+ input.tenant.app,
72
+ input.tenant.env,
73
+ input.tool,
74
+ JSON.stringify([...(input.claimableStateHandles ?? [])].sort()),
75
+ elevationDigest(continuation),
76
+ input.now.toISOString(),
77
+ new Date(input.now.getTime() + ASSISTANT_ELEVATION_TTL_MS).toISOString(),
78
+ ]);
79
+ const row = rows[0];
80
+ if (!row)
81
+ throw new Error('elevation insert returned no row');
82
+ return { elevation: toRecord(row), continuation };
83
+ }
84
+ async claim(input) {
85
+ const hash = elevationDigest(input.continuation);
86
+ const { rows } = await this.#pool.query(`UPDATE assistant_elevations
87
+ SET claimed_at = $2
88
+ WHERE continuation_hash = $1
89
+ AND claimed_at IS NULL
90
+ AND expires_at > $2
91
+ AND org_slug = $3 AND app_slug = $4 AND environment = $5
92
+ RETURNING *`, [hash, input.now.toISOString(), input.tenant.org, input.tenant.app, input.tenant.env]);
93
+ const claimed = rows[0];
94
+ if (claimed)
95
+ return { ok: true, elevation: toRecord(claimed) };
96
+ // Nothing was spent. Re-read to say *why* without consuming anything, so a wrong-tenant probe
97
+ // cannot deny the rightful owner their elevation.
98
+ const { rows: existing } = await this.#pool.query('SELECT * FROM assistant_elevations WHERE continuation_hash = $1', [hash]);
99
+ const row = existing[0];
100
+ if (!row || row.claimed_at !== null)
101
+ return { ok: false, reason: 'unknown' };
102
+ if (row.org_slug !== input.tenant.org ||
103
+ row.app_slug !== input.tenant.app ||
104
+ row.environment !== input.tenant.env) {
105
+ return { ok: false, reason: 'tenant_mismatch' };
106
+ }
107
+ return { ok: false, reason: 'expired' };
108
+ }
109
+ }
110
+ //# sourceMappingURL=postgres-elevation-store.js.map
@@ -0,0 +1,134 @@
1
+ import { newPublicEmbedId } from './in-memory-embed-store.js';
2
+ function toRecord(row) {
3
+ return {
4
+ embedId: row.embed_id,
5
+ org: row.org_slug,
6
+ app: row.app_slug,
7
+ env: row.environment,
8
+ surfaceMode: row.surface_mode,
9
+ createdAt: row.created_at,
10
+ ...(row.revoked_at === null ? {} : { revokedAt: row.revoked_at }),
11
+ // `bigint` arrives as a string; null stays undefined so "no override" and "zero" never merge.
12
+ ...(row.turns_per_day === null ? {} : { turnsPerDay: Number(row.turns_per_day) }),
13
+ ...(row.mints_per_day === null ? {} : { mintsPerDay: Number(row.mints_per_day) }),
14
+ ...(row.mints_per_address_hour === null
15
+ ? {}
16
+ : { mintsPerAddressHour: Number(row.mints_per_address_hour) }),
17
+ ...(row.turns_per_address_hour === null
18
+ ? {}
19
+ : { turnsPerAddressHour: Number(row.turns_per_address_hour) }),
20
+ ...(row.bridge_tool_calls_per_session === null
21
+ ? {}
22
+ : { bridgeToolCallsPerSession: Number(row.bridge_tool_calls_per_session) }),
23
+ ...(row.bridge_tool_calls_per_day === null
24
+ ? {}
25
+ : { bridgeToolCallsPerDay: Number(row.bridge_tool_calls_per_day) }),
26
+ };
27
+ }
28
+ /**
29
+ * Durable public embed identifiers.
30
+ *
31
+ * The table has **no secret column** — see `embed-store.ts` for why that is the point rather than an
32
+ * omission. A partial unique index over live rows is what makes `ensure` idempotent under concurrency:
33
+ * two simultaneous deploys of the same tenant surface cannot mint two live ids, and a revoked row does
34
+ * not block a later one, so revoke-then-redeploy issues a genuinely new id instead of reviving the old.
35
+ */
36
+ export class PostgresPublicEmbedStore {
37
+ #pool;
38
+ constructor(pool) {
39
+ this.#pool = pool;
40
+ }
41
+ async ensureSchema() {
42
+ await this.#pool.query(`
43
+ CREATE TABLE IF NOT EXISTS assistant_public_embeds (
44
+ embed_id text PRIMARY KEY,
45
+ org_slug text NOT NULL,
46
+ app_slug text NOT NULL,
47
+ environment text NOT NULL,
48
+ surface_mode text NOT NULL,
49
+ created_at timestamptz NOT NULL DEFAULT now(),
50
+ revoked_at timestamptz
51
+ )
52
+ `);
53
+ await this.#pool.query('ALTER TABLE assistant_public_embeds ADD COLUMN IF NOT EXISTS turns_per_day bigint');
54
+ await this.#pool.query('ALTER TABLE assistant_public_embeds ADD COLUMN IF NOT EXISTS mints_per_day bigint');
55
+ await this.#pool.query('ALTER TABLE assistant_public_embeds ADD COLUMN IF NOT EXISTS mints_per_address_hour bigint');
56
+ await this.#pool.query('ALTER TABLE assistant_public_embeds ADD COLUMN IF NOT EXISTS turns_per_address_hour bigint');
57
+ // The bridge budgets became operator-settable after they shipped platform-only (ADR 0220);
58
+ // additive like every column above it, so an existing row simply keeps using the defaults.
59
+ await this.#pool.query('ALTER TABLE assistant_public_embeds ADD COLUMN IF NOT EXISTS bridge_tool_calls_per_session bigint');
60
+ await this.#pool.query('ALTER TABLE assistant_public_embeds ADD COLUMN IF NOT EXISTS bridge_tool_calls_per_day bigint');
61
+ await this.#pool.query(`
62
+ CREATE UNIQUE INDEX IF NOT EXISTS assistant_public_embeds_live
63
+ ON assistant_public_embeds (org_slug, app_slug, environment)
64
+ WHERE revoked_at IS NULL
65
+ `);
66
+ }
67
+ async ensure(input) {
68
+ // `DO NOTHING` plus a follow-up read rather than `DO UPDATE`: a redeploy must return the *existing*
69
+ // id untouched, including its original `created_at`, because that id is already in page source.
70
+ const inserted = await this.#pool.query(`INSERT INTO assistant_public_embeds
71
+ (embed_id, org_slug, app_slug, environment, surface_mode, created_at)
72
+ SELECT $1, $2, $3, $4, $5, $6
73
+ WHERE $7::boolean OR NOT EXISTS (SELECT 1 FROM assistant_public_embeds
74
+ WHERE org_slug = $2 AND app_slug = $3 AND environment = $4)
75
+ ON CONFLICT DO NOTHING
76
+ RETURNING *`, [
77
+ newPublicEmbedId(),
78
+ input.org,
79
+ input.app,
80
+ input.env,
81
+ input.surfaceMode,
82
+ input.now,
83
+ input.allowRevokedReplacement !== false,
84
+ ]);
85
+ const row = inserted.rows[0];
86
+ if (row !== undefined)
87
+ return toRecord(row);
88
+ const existing = await this.#pool.query(`SELECT * FROM assistant_public_embeds
89
+ WHERE org_slug = $1 AND app_slug = $2 AND environment = $3 AND revoked_at IS NULL`, [input.org, input.app, input.env]);
90
+ const found = existing.rows[0];
91
+ if (found === undefined)
92
+ throw new Error('public embed id could not be resolved after insert');
93
+ return toRecord(found);
94
+ }
95
+ async setBudget(embedId, budget, _now) {
96
+ // COALESCE on the parameter, not the column: passing null leaves that cap as it was, so raising
97
+ // turns cannot quietly clear a mint ceiling someone set separately.
98
+ const result = await this.#pool.query(`UPDATE assistant_public_embeds
99
+ SET turns_per_day = COALESCE($2::bigint, turns_per_day),
100
+ mints_per_day = COALESCE($3::bigint, mints_per_day),
101
+ mints_per_address_hour = COALESCE($4::bigint, mints_per_address_hour),
102
+ turns_per_address_hour = COALESCE($5::bigint, turns_per_address_hour),
103
+ bridge_tool_calls_per_session = COALESCE($6::bigint, bridge_tool_calls_per_session),
104
+ bridge_tool_calls_per_day = COALESCE($7::bigint, bridge_tool_calls_per_day)
105
+ WHERE embed_id = $1 AND revoked_at IS NULL
106
+ RETURNING *`, [
107
+ embedId,
108
+ budget.turnsPerDay ?? null,
109
+ budget.mintsPerDay ?? null,
110
+ budget.mintsPerAddressHour ?? null,
111
+ budget.turnsPerAddressHour ?? null,
112
+ budget.bridgeToolCallsPerSession ?? null,
113
+ budget.bridgeToolCallsPerDay ?? null,
114
+ ]);
115
+ const row = result.rows[0];
116
+ return row === undefined ? undefined : toRecord(row);
117
+ }
118
+ async lookup(embedId) {
119
+ const result = await this.#pool.query('SELECT * FROM assistant_public_embeds WHERE embed_id = $1 AND revoked_at IS NULL', [embedId]);
120
+ const row = result.rows[0];
121
+ return row === undefined ? undefined : toRecord(row);
122
+ }
123
+ async list(tenant, options) {
124
+ const result = await this.#pool.query(`SELECT * FROM assistant_public_embeds
125
+ WHERE org_slug = $1 AND app_slug = $2 AND environment = $3 AND ($4::boolean OR revoked_at IS NULL)
126
+ ORDER BY created_at`, [tenant.org, tenant.app, tenant.env, options?.includeRevoked === true]);
127
+ return result.rows.map(toRecord);
128
+ }
129
+ async revoke(embedId, now) {
130
+ const result = await this.#pool.query('UPDATE assistant_public_embeds SET revoked_at = $2 WHERE embed_id = $1 AND revoked_at IS NULL', [embedId, now]);
131
+ return (result.rowCount ?? 0) > 0;
132
+ }
133
+ }
134
+ //# sourceMappingURL=postgres-embed-store.js.map
@@ -0,0 +1,11 @@
1
+ /** PostgreSQL assistant adapters for hosted service composition only. */
2
+ export * from './postgres-assistant.js';
3
+ export * from './postgres-assistant-appearance-store.js';
4
+ export * from './postgres-assistant-elevation.js';
5
+ export * from './postgres-assistant-interaction-writes.js';
6
+ export * from './postgres-assistant-interactions.js';
7
+ export * from './postgres-channel-store.js';
8
+ export * from './postgres-continuity-store.js';
9
+ export * from './postgres-elevation-store.js';
10
+ export * from './postgres-embed-store.js';
11
+ //# sourceMappingURL=postgres.js.map
@@ -39,4 +39,12 @@ export declare function surfaceBindingForOrigin(assistant: unknown, origin: stri
39
39
  readonly kind: 'pre-surfaces';
40
40
  };
41
41
  export declare function publicSurfaceOf(assistant: unknown): PublicSurface | undefined;
42
+ export interface MessagingSurface {
43
+ readonly kind: 'messaging';
44
+ readonly channel: 'whatsapp';
45
+ readonly mode: 'public';
46
+ readonly capabilities: readonly SurfaceCapabilityRef[];
47
+ readonly instructions?: string;
48
+ }
49
+ export declare function messagingSurfaceOf(assistant: unknown): MessagingSurface | undefined;
42
50
  //# sourceMappingURL=public-surface.d.ts.map
@@ -4,6 +4,8 @@ export function authenticatedSurfaceOf(assistant) {
4
4
  if (!Array.isArray(surfaces))
5
5
  return undefined;
6
6
  for (const entry of surfaces) {
7
+ if (entry?.kind === 'messaging')
8
+ continue;
7
9
  const { mode, origins, capabilities, instructions } = entry;
8
10
  if (mode !== 'authenticated')
9
11
  continue;
@@ -42,6 +44,8 @@ export function publicSurfaceOf(assistant) {
42
44
  if (!Array.isArray(surfaces))
43
45
  return undefined;
44
46
  for (const entry of surfaces) {
47
+ if (entry?.kind === 'messaging')
48
+ continue;
45
49
  const { mode, origins, capabilities, instructions } = entry;
46
50
  // A mixed surface admits strangers exactly as a public one does — sign-in only widens what a visitor
47
51
  // reaches after elevation, so both provision an embed id and both project.
@@ -61,4 +65,23 @@ export function publicSurfaceOf(assistant) {
61
65
  }
62
66
  return undefined;
63
67
  }
68
+ export function messagingSurfaceOf(assistant) {
69
+ const surfaces = assistant?.surfaces;
70
+ if (!Array.isArray(surfaces))
71
+ return undefined;
72
+ for (const entry of surfaces) {
73
+ if (entry?.kind !== 'messaging' || entry.channel !== 'whatsapp' || entry.mode !== 'public')
74
+ continue;
75
+ if (!Array.isArray(entry.capabilities))
76
+ return undefined;
77
+ return {
78
+ kind: 'messaging',
79
+ channel: 'whatsapp',
80
+ mode: 'public',
81
+ capabilities: entry.capabilities,
82
+ ...(typeof entry.instructions === 'string' ? { instructions: entry.instructions } : {}),
83
+ };
84
+ }
85
+ return undefined;
86
+ }
64
87
  //# sourceMappingURL=public-surface.js.map
@@ -162,6 +162,7 @@ export interface AssistantCapability {
162
162
  */
163
163
  export type AssistantSurfaceMode = 'authenticated' | 'public' | 'mixed';
164
164
  export interface AuthenticatedWebsiteAccess {
165
+ readonly kind?: 'website';
165
166
  readonly mode: 'authenticated';
166
167
  readonly origins: readonly string[];
167
168
  readonly instructions?: string;
@@ -199,6 +200,7 @@ export interface AssistantContinuityDeclaration {
199
200
  readonly maxRestores?: number;
200
201
  }
201
202
  export interface PublicWebsiteAccess {
203
+ readonly kind?: 'website';
202
204
  readonly mode: 'public' | 'mixed';
203
205
  readonly origins: readonly string[];
204
206
  readonly capabilities: readonly CapabilityRef[];
@@ -210,7 +212,19 @@ export interface PublicWebsiteAccess {
210
212
  /** @see PublicWebsiteInput.continuity */
211
213
  readonly continuity?: AssistantContinuityDeclaration;
212
214
  }
213
- export type AssistantAccess = AuthenticatedWebsiteAccess | PublicWebsiteAccess;
215
+ export interface PublicMessagingInput {
216
+ readonly channel: 'whatsapp';
217
+ readonly capabilities: readonly CapabilityRef[];
218
+ readonly instructions?: string;
219
+ }
220
+ export interface PublicMessagingAccess extends Omit<PublicMessagingInput, 'capabilities'> {
221
+ readonly capabilities: readonly AssistantCapability[];
222
+ readonly kind: 'messaging';
223
+ readonly mode: 'public';
224
+ }
225
+ /** Public business capability intent. Provider assets and credentials are operator-owned. */
226
+ export declare function publicMessaging(input: PublicMessagingInput): PublicMessagingAccess;
227
+ export type AssistantAccess = AuthenticatedWebsiteAccess | PublicWebsiteAccess | PublicMessagingAccess;
214
228
  export interface AuthenticatedWebsiteInput {
215
229
  readonly origins: readonly (string | ConfigRef)[];
216
230
  /** Assistant-only behavior for this exact signed-in website audience. */
@@ -281,6 +295,7 @@ export interface EmbeddedAssistantOptions extends AssistantUiOptions {
281
295
  }
282
296
  /** One projected surface as it appears in compiled data. */
283
297
  export interface AssistantSurfaceConfig {
298
+ readonly kind?: 'website';
284
299
  readonly mode: AssistantSurfaceMode;
285
300
  readonly origins: readonly string[];
286
301
  readonly instructions?: string;
@@ -299,7 +314,7 @@ export interface AssistantSurfaceConfig {
299
314
  }
300
315
  export interface EmbeddedAssistantConfig extends AssistantUiOptions {
301
316
  readonly model: AssistantModel;
302
- readonly surfaces: readonly AssistantSurfaceConfig[];
317
+ readonly surfaces: readonly (AssistantSurfaceConfig | PublicMessagingAccess)[];
303
318
  /**
304
319
  * Union of every surface's origins. Kept at the top level so the service's origin check and the
305
320
  * published session-exchange contract read on unchanged while surfaces are added.
@@ -1,4 +1,24 @@
1
1
  import { serializeSecretRef, serializeVariableRef } from './config.js';
2
+ /** Public business capability intent. Provider assets and credentials are operator-owned. */
3
+ export function publicMessaging(input) {
4
+ if (!input || !Array.isArray(input.capabilities)) {
5
+ throw new Error('publicMessaging requires an explicit capability allowlist');
6
+ }
7
+ for (const key of Object.keys(input)) {
8
+ if (!['channel', 'capabilities', 'instructions'].includes(key)) {
9
+ throw new Error(`publicMessaging does not accept ${key}`);
10
+ }
11
+ }
12
+ if (input.channel !== 'whatsapp')
13
+ throw new Error('Unsupported public messaging channel');
14
+ return {
15
+ kind: 'messaging',
16
+ mode: 'public',
17
+ channel: input.channel,
18
+ capabilities: normalizeCapabilities(input.capabilities),
19
+ ...(input.instructions === undefined ? {} : { instructions: input.instructions }),
20
+ };
21
+ }
2
22
  export function authenticatedWebsite(input) {
3
23
  return {
4
24
  mode: 'authenticated',
@@ -49,22 +69,27 @@ export function embeddedAssistant(input) {
49
69
  const sessionClaims = surfaces.find((surface) => surface.mode === 'authenticated')?.sessionClaims;
50
70
  return {
51
71
  model: { ...model },
52
- surfaces: surfaces.map((surface) => ({
53
- mode: surface.mode,
54
- origins: [...surface.origins],
55
- ...(surface.instructions === undefined ? {} : { instructions: surface.instructions }),
56
- ...(surface.capabilities
57
- ? { capabilities: normalizeCapabilities(surface.capabilities) }
58
- : {}),
59
- ...(surface.mode === 'authenticated' && surface.sessionClaims
60
- ? { sessionClaims: structuredClone(surface.sessionClaims) }
61
- : {}),
62
- ...(surface.webmcp === undefined ? {} : { webmcp: { ...surface.webmcp } }),
63
- ...(surface.mode !== 'authenticated' && surface.continuity !== undefined
64
- ? { continuity: { ...surface.continuity } }
65
- : {}),
66
- })),
67
- allowedOrigins: surfaces.flatMap((surface) => [...surface.origins]),
72
+ surfaces: surfaces.map((surface) => surface.kind === 'messaging'
73
+ ? {
74
+ ...surface,
75
+ capabilities: normalizeCapabilities(surface.capabilities),
76
+ }
77
+ : {
78
+ mode: surface.mode,
79
+ origins: [...surface.origins],
80
+ ...(surface.instructions === undefined ? {} : { instructions: surface.instructions }),
81
+ ...(surface.capabilities
82
+ ? { capabilities: normalizeCapabilities(surface.capabilities) }
83
+ : {}),
84
+ ...(surface.mode === 'authenticated' && surface.sessionClaims
85
+ ? { sessionClaims: structuredClone(surface.sessionClaims) }
86
+ : {}),
87
+ ...(surface.webmcp === undefined ? {} : { webmcp: { ...surface.webmcp } }),
88
+ ...(surface.mode !== 'authenticated' && surface.continuity !== undefined
89
+ ? { continuity: { ...surface.continuity } }
90
+ : {}),
91
+ }),
92
+ allowedOrigins: surfaces.flatMap((surface) => surface.kind === 'messaging' ? [] : [...surface.origins]),
68
93
  ...structuredClone(ui),
69
94
  ...(suggestedPrompts ? { suggestedPrompts: [...suggestedPrompts] } : {}),
70
95
  ...(sessionClaims ? { sessionClaims: structuredClone(sessionClaims) } : {}),
@@ -109,15 +134,19 @@ function assertDistinctSurfaces(surfaces) {
109
134
  if (surfaces.length === 0) {
110
135
  throw new Error('embeddedAssistant requires at least one access surface');
111
136
  }
112
- const publicSurfaces = surfaces.filter((surface) => surface.mode !== 'authenticated');
137
+ const websites = surfaces.filter((surface) => surface.kind !== 'messaging');
138
+ const messaging = surfaces.filter((surface) => surface.kind === 'messaging');
139
+ if (messaging.length > 1)
140
+ throw new Error('embeddedAssistant accepts only one whatsapp surface');
141
+ const publicSurfaces = websites.filter((surface) => surface.mode !== 'authenticated');
113
142
  if (publicSurfaces.length > 1) {
114
143
  throw new Error('embeddedAssistant accepts at most one public surface (public or mixed)');
115
144
  }
116
- if (surfaces.length - publicSurfaces.length > 1) {
145
+ if (websites.length - publicSurfaces.length > 1) {
117
146
  throw new Error('embeddedAssistant accepts at most one authenticated surface');
118
147
  }
119
148
  const seen = new Set();
120
- for (const surface of surfaces) {
149
+ for (const surface of websites) {
121
150
  for (const origin of surface.origins) {
122
151
  if (seen.has(origin)) {
123
152
  throw new Error(`embeddedAssistant surfaces must not share an origin: ${origin}`);
@@ -2,7 +2,7 @@ import type { ConnectorFile, HttpConnectorDef } from '@noodle-borg/connector-def
2
2
  import { z } from 'zod';
3
3
  export type { PackagedAssetReference } from '@noodle-borg/compiler';
4
4
  export type { AgentCapabilityKind, AgentCapabilityRef, AgentExampleSource, AgentGuideSource, AgentWorkflowSource, AgentWorkflowStepSource, } from './agent-guide.js';
5
- export { type AssistantAccess, type AssistantCapability, type AssistantLabels, type AssistantLayoutMode, type AssistantModel, type AssistantPosition, type AssistantPresentationOptions, type AssistantPresentationTone, type AssistantSurfaceConfig, type AssistantSurfaceMode, type AssistantThemeMode, type AssistantUiOptions, type AuthenticatedWebsiteAccess, type AuthenticatedWebsiteInput, authenticatedWebsite, type CapabilityRef, type EmbeddedAssistantConfig, type EmbeddedAssistantOptions, embeddedAssistant, type NoodleManagedModel, noodleManaged, type OpenAICompatibleModel, type OpenAICompatibleModelInput, type OpenAICompatibleTransport, openAICompatible, type PublicWebsiteAccess, type PublicWebsiteInput, publicWebsite, } from './assistant.js';
5
+ export { type AssistantAccess, type AssistantCapability, type AssistantLabels, type AssistantLayoutMode, type AssistantModel, type AssistantPosition, type AssistantPresentationOptions, type AssistantPresentationTone, type AssistantSurfaceConfig, type AssistantSurfaceMode, type AssistantThemeMode, type AssistantUiOptions, type AuthenticatedWebsiteAccess, type AuthenticatedWebsiteInput, authenticatedWebsite, type CapabilityRef, type EmbeddedAssistantConfig, type EmbeddedAssistantOptions, embeddedAssistant, type NoodleManagedModel, noodleManaged, type OpenAICompatibleModel, type OpenAICompatibleModelInput, type OpenAICompatibleTransport, openAICompatible, type PublicMessagingAccess, type PublicMessagingInput, type PublicWebsiteAccess, type PublicWebsiteInput, publicMessaging, publicWebsite, } from './assistant.js';
6
6
  export { type ConfigRef, type ConfigRefKind, type DeclaredVariableRef, secret, type VariableOptions, type VariableValueRef, variable, } from './config.js';
7
7
  export { type BoundConnectorRef, bind, type ConnectionBinding, type ConnectionRef, type ConnectionSource, clientCredentials, connection, externalExchange, type GoogleWorkloadIdentityAccess, googleWorkloadIdentity, managedSecret, } from './connections.js';
8
8
  export { type ComputeHost, type ComputeLimits, type ComputeOperationOptions, ConnectorBuilder, type ConnectorCatalogDoc, type ConnectorOpDefinition, type ConnectorOperationOptions, type ConnectorRef, connector, type HttpConnectorOptions, type McpAuthOptions, type McpConnectorOptions, } from './connectors.js';
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export { authenticatedWebsite, embeddedAssistant, noodleManaged, openAICompatible, publicWebsite, } from './assistant.js';
2
+ export { authenticatedWebsite, embeddedAssistant, noodleManaged, openAICompatible, publicMessaging, publicWebsite, } from './assistant.js';
3
3
  export { secret, variable, } from './config.js';
4
4
  export { bind, clientCredentials, connection, externalExchange, googleWorkloadIdentity, managedSecret, } from './connections.js';
5
5
  export { ConnectorBuilder, connector, } from './connectors.js';
@@ -33,7 +33,7 @@ export function prepareAuthoringRuntimeDir(dir, options) {
33
33
  './react': './react.mjs',
34
34
  },
35
35
  }));
36
- writeFileSync(join(self, 'index.mjs'), "export { algolia, annotations, asset, authenticatedWebsite, bind, clientCredentials, connector, connection, customerAuth, customerEndpoint, embeddedAssistant, externalExchange, file, firecrawl, gmailConnector, googleWorkloadIdentity, handoffSession, knowledge, managedCollection, managedSecret, meilisearch, noodleManaged, noodlePlatform, openAICompatible, prompt, publicWebsite, resource, secret, server, site, tavily, tool, variable, webExtract, when, z } from '@noodle-borg/authoring';\n");
36
+ writeFileSync(join(self, 'index.mjs'), "export { algolia, annotations, asset, authenticatedWebsite, bind, clientCredentials, connector, connection, customerAuth, customerEndpoint, embeddedAssistant, externalExchange, file, firecrawl, gmailConnector, googleWorkloadIdentity, handoffSession, knowledge, managedCollection, managedSecret, meilisearch, noodleManaged, noodlePlatform, openAICompatible, prompt, publicMessaging, publicWebsite, resource, secret, server, site, tavily, tool, variable, webExtract, when, z } from '@noodle-borg/authoring';\n");
37
37
  writeFileSync(join(self, 'platform.mjs'), "export { noodlePlatform, noodlePlatformCatalog } from '@noodle-borg/authoring';\n");
38
38
  writeFileSync(join(self, 'react.mjs'), "export * from '@noodle-borg/authoring/react';\n");
39
39
  }