@noodleseed/one 0.173.0 → 0.175.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 (170) hide show
  1. package/dist/commands/solutions-activity.js +1 -1
  2. package/dist/commands/solutions-activity.js.map +1 -1
  3. package/dist/commands/solutions-drafts.d.ts.map +1 -1
  4. package/dist/commands/solutions-drafts.js +36 -9
  5. package/dist/commands/solutions-drafts.js.map +1 -1
  6. package/dist/commands/solutions-native-lifecycle.d.ts +5 -0
  7. package/dist/commands/solutions-native-lifecycle.d.ts.map +1 -0
  8. package/dist/commands/solutions-native-lifecycle.js +77 -0
  9. package/dist/commands/solutions-native-lifecycle.js.map +1 -0
  10. package/dist/commands/solutions-ops-args.d.ts +42 -0
  11. package/dist/commands/solutions-ops-args.d.ts.map +1 -0
  12. package/dist/commands/solutions-ops-args.js +47 -0
  13. package/dist/commands/solutions-ops-args.js.map +1 -0
  14. package/dist/commands/solutions-ops.d.ts.map +1 -1
  15. package/dist/commands/solutions-ops.js +9 -46
  16. package/dist/commands/solutions-ops.js.map +1 -1
  17. package/dist/commands/solutions-workspace.d.ts +5 -0
  18. package/dist/commands/solutions-workspace.d.ts.map +1 -0
  19. package/dist/commands/solutions-workspace.js +157 -0
  20. package/dist/commands/solutions-workspace.js.map +1 -0
  21. package/dist/commands/whatsapp-output.d.ts.map +1 -1
  22. package/dist/commands/whatsapp-output.js +20 -4
  23. package/dist/commands/whatsapp-output.js.map +1 -1
  24. package/node_modules/@noodle-borg/admission-limits/dist/public-record-admission.d.ts +16 -9
  25. package/node_modules/@noodle-borg/admission-limits/dist/public-record-admission.js +5 -2
  26. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +9 -0
  27. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +5 -5
  28. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +9 -0
  29. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +1 -1
  30. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +13 -1
  31. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +5 -3
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +5 -4
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +6 -2
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +1 -1
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +12 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +7 -2
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +14 -4
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-capture.js +104 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-judgment.js +93 -0
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-ledger.d.ts +105 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-ledger.js +183 -0
  43. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-model-call.js +38 -0
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-parsers.d.ts +22 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-parsers.js +109 -0
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-review.d.ts +21 -0
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-review.js +44 -0
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-spec.d.ts +18 -0
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-spec.js +86 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-store.d.ts +11 -0
  51. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-store.js +18 -0
  52. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +20 -1
  53. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +38 -2
  54. package/node_modules/@noodle-borg/assistant-gateway/dist/model-runtime.js +3 -0
  55. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +5 -0
  56. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +5 -0
  57. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
  58. package/node_modules/@noodle-borg/authoring/dist/interaction.d.ts +49 -0
  59. package/node_modules/@noodle-borg/authoring/dist/interaction.js +25 -0
  60. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
  61. package/node_modules/@noodle-borg/authoring/dist/server.js +4 -0
  62. package/node_modules/@noodle-borg/authoring/package.json +6 -1
  63. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +1 -1
  64. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-drafts.js +7 -0
  65. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-native-lifecycle.js +43 -0
  66. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-records.js +154 -0
  67. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-workspace.js +95 -0
  68. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +4 -149
  69. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +2 -1
  70. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +33 -0
  71. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
  72. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
  73. package/node_modules/@noodle-borg/compiler/dist/compile.js +5 -0
  74. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  75. package/node_modules/@noodle-borg/compiler/dist/index.d.ts +2 -2
  76. package/node_modules/@noodle-borg/compiler/dist/index.js +1 -1
  77. package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-schema.d.ts +41 -0
  78. package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-schema.js +62 -0
  79. package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-validation.js +220 -0
  80. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +60 -4
  81. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +7 -1
  82. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +9 -1
  83. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +58 -10
  84. package/node_modules/@noodle-borg/control-plane/dist/in-memory-atomic-state.d.ts +8 -0
  85. package/node_modules/@noodle-borg/control-plane/dist/in-memory-atomic-state.js +136 -0
  86. package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.d.ts +7 -0
  87. package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.js +16 -24
  88. package/node_modules/@noodle-borg/control-plane/dist/in-memory-organization-store.d.ts +4 -2
  89. package/node_modules/@noodle-borg/control-plane/dist/in-memory-organization-store.js +8 -6
  90. package/node_modules/@noodle-borg/control-plane/dist/mcp-subdomain-claims.d.ts +2 -1
  91. package/node_modules/@noodle-borg/control-plane/dist/mcp-subdomain-claims.js +13 -8
  92. package/node_modules/@noodle-borg/control-plane/dist/organization-agreements.d.ts +13 -4
  93. package/node_modules/@noodle-borg/control-plane/dist/organization-agreements.js +23 -7
  94. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +2 -1
  95. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  96. package/node_modules/@noodle-borg/module/dist/platform-human-identity.d.ts +5 -2
  97. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +16 -4
  98. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +3 -6
  99. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
  100. package/node_modules/@noodle-borg/service/dist/application-activity.js +85 -63
  101. package/node_modules/@noodle-borg/service/dist/application-drafts/contracts.js +9 -0
  102. package/node_modules/@noodle-borg/service/dist/application-drafts/paths.js +2 -2
  103. package/node_modules/@noodle-borg/service/dist/application-runtime-target.js +1 -1
  104. package/node_modules/@noodle-borg/service/dist/business-information/contracts.d.ts +18 -4
  105. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-business-pages.js +3 -5
  106. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-installation-lifecycle.js +8 -6
  107. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-native-lifecycle.js +80 -0
  108. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +27 -31
  109. package/node_modules/@noodle-borg/service/dist/business-information/memory-native-retention.js +29 -0
  110. package/node_modules/@noodle-borg/service/dist/business-information/model.js +9 -2
  111. package/node_modules/@noodle-borg/service/dist/business-information/native-query.js +3 -1
  112. package/node_modules/@noodle-borg/service/dist/business-information/native-record-lifecycle.d.ts +27 -0
  113. package/node_modules/@noodle-borg/service/dist/business-information/native-record-lifecycle.js +63 -0
  114. package/node_modules/@noodle-borg/service/dist/business-information/principal-authority.d.ts +3 -1
  115. package/node_modules/@noodle-borg/service/dist/business-information/principal-authority.js +9 -4
  116. package/node_modules/@noodle-borg/service/dist/business-information/staff-authority.d.ts +16 -0
  117. package/node_modules/@noodle-borg/service/dist/business-information/staff-authority.js +60 -0
  118. package/node_modules/@noodle-borg/service/dist/business-information/workspace-permissions.js +18 -0
  119. package/node_modules/@noodle-borg/service/dist/business-information-runtime.js +1 -0
  120. package/node_modules/@noodle-borg/service/dist/business-onboarding.js +16 -3
  121. package/node_modules/@noodle-borg/service/dist/business-workspaces/contracts.d.ts +110 -0
  122. package/node_modules/@noodle-borg/service/dist/business-workspaces/contracts.js +52 -0
  123. package/node_modules/@noodle-borg/service/dist/business-workspaces/paths.js +17 -0
  124. package/node_modules/@noodle-borg/service/dist/business-workspaces/permissions.d.ts +7 -0
  125. package/node_modules/@noodle-borg/service/dist/business-workspaces/store.d.ts +102 -0
  126. package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +70 -8
  127. package/node_modules/@noodle-borg/service/dist/channels/capability-report.js +73 -0
  128. package/node_modules/@noodle-borg/service/dist/channels/collection-turn.js +232 -0
  129. package/node_modules/@noodle-borg/service/dist/channels/composition.js +1 -0
  130. package/node_modules/@noodle-borg/service/dist/channels/messaging-eligibility.js +45 -0
  131. package/node_modules/@noodle-borg/service/dist/channels/reply-buttons.js +23 -0
  132. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +127 -33
  133. package/node_modules/@noodle-borg/service/dist/native-record-connector.js +19 -4
  134. package/node_modules/@noodle-borg/service/dist/routes/application-drafts.js +44 -2
  135. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +4 -0
  136. package/node_modules/@noodle-borg/service/dist/routes/business-authoring-dispatch.js +23 -0
  137. package/node_modules/@noodle-borg/service/dist/routes/business-information-access.js +39 -0
  138. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +1 -1
  139. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +2 -2
  140. package/node_modules/@noodle-borg/service/dist/routes/business-information-connections.js +10 -4
  141. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +11 -6
  142. package/node_modules/@noodle-borg/service/dist/routes/business-information-installation.js +3 -1
  143. package/node_modules/@noodle-borg/service/dist/routes/business-information-notice.js +6 -2
  144. package/node_modules/@noodle-borg/service/dist/routes/business-information-pages.js +1 -1
  145. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +1 -1
  146. package/node_modules/@noodle-borg/service/dist/routes/business-information-record-create.js +7 -2
  147. package/node_modules/@noodle-borg/service/dist/routes/business-information-record-reads.js +2 -2
  148. package/node_modules/@noodle-borg/service/dist/routes/business-information-records.js +332 -0
  149. package/node_modules/@noodle-borg/service/dist/routes/business-information-settings.js +6 -3
  150. package/node_modules/@noodle-borg/service/dist/routes/business-information-staff.js +9 -5
  151. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +12 -356
  152. package/node_modules/@noodle-borg/service/dist/routes/business-native-lifecycle.js +46 -0
  153. package/node_modules/@noodle-borg/service/dist/routes/business-workspaces.js +119 -0
  154. package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +2 -1
  155. package/node_modules/@noodle-borg/service/dist/service.js +9 -2
  156. package/node_modules/@noodle-borg/service/dist/web-capabilities.js +3 -1
  157. package/node_modules/@noodle-borg/service/package.json +2 -1
  158. package/node_modules/@noodle-borg/wire-contracts/dist/application-draft-validation.d.ts +59 -0
  159. package/node_modules/@noodle-borg/wire-contracts/dist/application-draft-validation.js +38 -0
  160. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +144 -50
  161. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +44 -4
  162. package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.d.ts +258 -0
  163. package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.js +118 -1
  164. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +2 -0
  165. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +2 -0
  166. package/node_modules/@noodle-borg/wire-contracts/dist/native-record-lifecycle.d.ts +81 -0
  167. package/node_modules/@noodle-borg/wire-contracts/dist/native-record-lifecycle.js +39 -0
  168. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +26 -0
  169. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +24 -1
  170. package/package.json +2 -2
@@ -20,6 +20,17 @@ const valueSchema = z.object({
20
20
  timestamp,
21
21
  type: z.string().max(64),
22
22
  text: z.object({ body: z.string().max(1 << 20) }).optional(),
23
+ // A tapped reply button arrives as type "interactive" with interactive.type "button_reply"
24
+ // carrying the id and title we sent (360dialog webhook reference, "Received Answer to Reply
25
+ // Button", checked 2026-09-21). Other interactive kinds are not conversation input here.
26
+ interactive: z
27
+ .object({
28
+ type: z.string().max(64),
29
+ button_reply: z
30
+ .object({ id: z.string().min(1).max(256), title: z.string().max(64) })
31
+ .optional(),
32
+ })
33
+ .optional(),
23
34
  }))
24
35
  .max(100)
25
36
  .optional(),
@@ -74,11 +85,15 @@ export function parseWhatsAppWebhook(input, phoneNumberId) {
74
85
  address = { kind: 'opaque', value: message.user_id };
75
86
  else
76
87
  throw new ChannelError('recipient_invalid');
88
+ const button = message.type === 'interactive' && message.interactive?.type === 'button_reply'
89
+ ? message.interactive.button_reply
90
+ : undefined;
77
91
  messages.push({
78
92
  providerId: message.id,
79
93
  address,
80
94
  eventAt: message.timestamp,
81
95
  ...(message.type === 'text' && message.text ? { text: message.text.body } : {}),
96
+ ...(button ? { button: { id: button.id, title: button.title } } : {}),
82
97
  });
83
98
  }
84
99
  for (const status of value.statuses ?? [])
@@ -88,6 +103,10 @@ export function parseWhatsAppWebhook(input, phoneNumberId) {
88
103
  }
89
104
  return { messages, statuses };
90
105
  }
106
+ /** A 4xx that judges the payload itself; auth, throttling and timeouts are no reason to resend differently. */
107
+ function rejectedPayload(status) {
108
+ return status >= 400 && status < 500 && ![401, 403, 408, 429].includes(status);
109
+ }
91
110
  async function boundedJson(response) {
92
111
  const reader = response.body?.getReader();
93
112
  if (!reader)
@@ -205,17 +224,56 @@ export class Dialog360 {
205
224
  return { state: 'unknown', code: 'provider_outcome_unknown' };
206
225
  }
207
226
  }
227
+ /**
228
+ * One reply. With buttons the review goes as an interactive reply-button message: at most three
229
+ * buttons, titles up to 20 characters, ids up to 256 and a body up to 1024 characters, per the
230
+ * Meta Cloud API reply-buttons reference that the 360dialog Cloud API host relays (360dialog's own
231
+ * interactive-messages page states only the three-button limit; its webhook reference gives the
232
+ * inbound shape); all checked 2026-09-21. A payload the provider rejects outright dispatched
233
+ * nothing, so the same text goes plain instead; an ambiguous outcome is never retried.
234
+ */
208
235
  async send(input) {
209
236
  if (!input.text || input.text.length > 4096)
210
237
  return { state: 'failed', code: 'reply_invalid' };
238
+ const buttons = input.buttons ?? [];
239
+ const interactive = buttons.length > 0 &&
240
+ buttons.length <= 3 &&
241
+ input.text.length <= 1024 &&
242
+ buttons.every((button) => button.id.length <= 256 && button.title.length <= 20);
243
+ const envelope = {
244
+ messaging_product: 'whatsapp',
245
+ recipient_type: 'individual',
246
+ ...(input.to.kind === 'phone' ? { to: input.to.value } : { recipient: input.to.value }),
247
+ };
211
248
  try {
212
- const response = await this.request('/messages', 'POST', {
213
- messaging_product: 'whatsapp',
214
- recipient_type: 'individual',
215
- type: 'text',
216
- ...(input.to.kind === 'phone' ? { to: input.to.value } : { recipient: input.to.value }),
217
- text: { body: input.text, preview_url: false },
218
- });
249
+ let code;
250
+ let response = interactive
251
+ ? await this.request('/messages', 'POST', {
252
+ ...envelope,
253
+ type: 'interactive',
254
+ interactive: {
255
+ type: 'button',
256
+ body: { text: input.text },
257
+ action: {
258
+ buttons: buttons.map((button) => ({
259
+ type: 'reply',
260
+ reply: { id: button.id, title: button.title },
261
+ })),
262
+ },
263
+ },
264
+ })
265
+ : undefined;
266
+ if (response !== undefined && !response.ok && rejectedPayload(response.status)) {
267
+ await response.body?.cancel();
268
+ code = 'interactive_rejected';
269
+ response = undefined;
270
+ }
271
+ if (response === undefined)
272
+ response = await this.request('/messages', 'POST', {
273
+ ...envelope,
274
+ type: 'text',
275
+ text: { body: input.text, preview_url: false },
276
+ });
219
277
  if (!response.ok) {
220
278
  await response.body?.cancel();
221
279
  return {
@@ -227,7 +285,11 @@ export class Dialog360 {
227
285
  .object({ messages: z.array(z.object({ id: identifier })).length(1) })
228
286
  .safeParse(await boundedJson(response));
229
287
  return parsed.success
230
- ? { state: 'accepted', providerMessageId: parsed.data.messages[0].id }
288
+ ? {
289
+ state: 'accepted',
290
+ providerMessageId: parsed.data.messages[0].id,
291
+ ...(code === undefined ? {} : { code }),
292
+ }
231
293
  : { state: 'unknown', code: 'provider_receipt_invalid' };
232
294
  }
233
295
  catch {
@@ -0,0 +1,73 @@
1
+ const ACCOUNT_SELF_SERVICE = {
2
+ capability: 'account_self_service',
3
+ status: 'unavailable',
4
+ code: 'IDENTITY_NOT_ESTABLISHABLE',
5
+ requirement: 'verified_customer',
6
+ next: 'Account questions are answered as limitations until WhatsApp identity linking ships.',
7
+ };
8
+ export async function capabilityReport(input) {
9
+ const { artifact } = input;
10
+ if (artifact === undefined)
11
+ return [ACCOUNT_SELF_SERVICE];
12
+ const reports = [];
13
+ const interactions = artifact.toolInteractions ?? {};
14
+ const present = new Set(artifact.tools.map((tool) => tool.name));
15
+ const openers = Object.entries(interactions).filter(([opener, interaction]) => present.has(opener) && present.has(interaction.action));
16
+ const actions = new Set(openers.map(([, interaction]) => interaction.action));
17
+ if ((artifact.server.knowledge?.length ?? 0) > 0)
18
+ reports.push({ capability: 'answer_questions', status: 'native' });
19
+ const reads = artifact.tools.filter((tool) => tool.annotations?.readOnlyHint === true &&
20
+ interactions[tool.name] === undefined &&
21
+ !actions.has(tool.name));
22
+ if (reads.length > 0)
23
+ reports.push({ capability: 'look_up_information', status: 'native' });
24
+ if (openers.length > 0) {
25
+ const setup = await captureSetup(input, artifact, openers);
26
+ reports.push({ capability: 'capture_request', ...(setup ?? { status: 'native' }) });
27
+ }
28
+ reports.push(ACCOUNT_SELF_SERVICE);
29
+ return reports;
30
+ }
31
+ /** The first missing prerequisite of any selected collect action, mirroring the record connector's own gates. */
32
+ async function captureSetup(input, artifact, openers) {
33
+ const needs = (code, requirement, next) => ({
34
+ status: 'needs_setup',
35
+ code,
36
+ requirement,
37
+ next,
38
+ });
39
+ if (!input.durable)
40
+ return needs('durable_storage_required', 'durable_interaction_store', 'Run the service with PostgreSQL channel storage, then rerun doctor.');
41
+ if (input.installations === undefined)
42
+ return needs('business_information_unavailable', 'collection_installation', 'Enable managed business information on this service, then rerun doctor.');
43
+ const installations = (await input.installations.listInstallations(input.tenant.org)).filter((item) => item.scope.app === input.tenant.app && item.scope.env === input.tenant.env);
44
+ const installation = installations.length === 1 ? installations[0] : undefined;
45
+ if (installation === undefined)
46
+ return needs('collection_not_installed', 'collection_installation', "Install the application's collections for this environment, then rerun doctor.");
47
+ for (const [, interaction] of openers) {
48
+ const action = artifact.tools.find((tool) => tool.name === interaction.action);
49
+ const collection = action === undefined ? undefined : targetCollection(action.fulfilment);
50
+ if (collection !== undefined && !enabled(installation, collection))
51
+ return needs('collection_not_installed', 'collection_installation', `Enable the "${collection}" collection on the installation, then rerun doctor.`);
52
+ }
53
+ if (!installation.intakeActive)
54
+ return needs('intake_inactive', 'public_intake', 'Resume public intake for the installation, then rerun doctor.');
55
+ return undefined;
56
+ }
57
+ function enabled(installation, collection) {
58
+ return (installation.managedCollections.includes(collection) &&
59
+ installation.definition.collections.some((entry) => entry.key === collection));
60
+ }
61
+ /** The literal `collection` argument of the action's single native-record operation, when readable. */
62
+ function targetCollection(fulfilment) {
63
+ const args = fulfilment.kind === 'operation'
64
+ ? [fulfilment.args]
65
+ : fulfilment.steps.flatMap((step) => (step.kind === 'operation' ? [step.args] : []));
66
+ for (const map of args) {
67
+ const node = map.collection;
68
+ if (node?.kind === 'literal' && typeof node.value === 'string')
69
+ return node.value;
70
+ }
71
+ return undefined;
72
+ }
73
+ //# sourceMappingURL=capability-report.js.map
@@ -0,0 +1,232 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CONFIRMATION_AFFIRMATIVES, CORRECTION_CUES, capture, confirmationReply, consentReply, normaliseReply, } from '@noodle-borg/assistant-gateway/model-runtime';
3
+ import { applyConfirmation, applyConsent, attachProposal, cancelCollection, channelDigest, completeCollection, deleteCollection, expireCollection, ledgerForModel, loadCollection, pendingProposal, redactSpans, rejectField, renderReview, saveCollection, scanTypedValues, withdrawProposal, } from '@noodle-borg/assistant-gateway/portable';
4
+ const PARSED = new Set(['email', 'phone', 'url']);
5
+ function isParsed(control) {
6
+ return PARSED.has(control);
7
+ }
8
+ const reply = (ledger, text) => ({
9
+ kind: 'reply',
10
+ ledger,
11
+ reply: text,
12
+ });
13
+ const IGNORED = {
14
+ kind: 'ignored',
15
+ code: 'button_unbound',
16
+ };
17
+ export async function runCollectionTurn(open, text, deps,
18
+ /** The opaque id of a tapped review button; the text is then not conversation input. */
19
+ button) {
20
+ const { spec, now } = deps;
21
+ // Re-entry after a crash between the durable claim and the receipt: the write may already have
22
+ // committed, so no clock governs this phase; the same identity re-executes and the connector replays.
23
+ if (open.phase === 'executing')
24
+ return execute(open, deps);
25
+ const ledger = expireCollection(open, now);
26
+ if (ledger.phase === 'expired') {
27
+ await deps.remove();
28
+ return button === undefined ? { kind: 'none' } : IGNORED;
29
+ }
30
+ if (ledger !== open) {
31
+ // The pending proposal lapsed: nothing was sent, and the participant may start over.
32
+ const cancelled = cancelCollection(ledger, now);
33
+ await deps.persist(cancelled);
34
+ return reply(cancelled, 'That review expired before it was confirmed, so nothing was sent. Tell me if you would like to start again.');
35
+ }
36
+ switch (ledger.phase) {
37
+ case 'completed': {
38
+ // A repeated confirmation inside the window, typed or tapped on the executed review's own
39
+ // button, is the same confirmation: the receipt is echoed without a model call or a write.
40
+ const within = Date.parse(ledger.updatedAt) + spec.confirmationExpiryMs > now;
41
+ const echo = within &&
42
+ (button === undefined
43
+ ? CONFIRMATION_AFFIRMATIVES.includes(normaliseReply(text))
44
+ : ledger.executedProposalId !== undefined &&
45
+ deps.buttons?.act(ledger.executedProposalId, button) === 'confirm');
46
+ if (echo)
47
+ return reply(ledger, spec.successMessage);
48
+ await deps.remove();
49
+ return button === undefined ? { kind: 'none' } : IGNORED;
50
+ }
51
+ case 'cancelled':
52
+ await deps.remove();
53
+ return button === undefined ? { kind: 'none' } : IGNORED;
54
+ case 'awaiting_confirmation': {
55
+ const proposal = pendingProposal(ledger, now);
56
+ // Nothing is pending because the participant withdrew the proposal to edit: capture first.
57
+ if (proposal === undefined)
58
+ return button === undefined ? collect(ledger, text, deps) : IGNORED;
59
+ const act = button === undefined
60
+ ? await confirmationReply(text, spec, deps.model)
61
+ : deps.buttons?.act(proposal.id, button);
62
+ if (act === undefined)
63
+ return IGNORED;
64
+ if (act === 'confirm')
65
+ return execute(applyConfirmation(ledger, 'confirm', now), deps);
66
+ if (act === 'cancel') {
67
+ const cancelled = cancelCollection(ledger, now);
68
+ await deps.persist(cancelled);
69
+ return reply(cancelled, 'Okay, I have cancelled that. Nothing was sent.');
70
+ }
71
+ if (act === 'unclear')
72
+ return reply(ledger, 'Shall I send it? Yes or no.');
73
+ if (button !== undefined) {
74
+ // Edit: this proposal and its buttons lapse now; the next message is captured as the change.
75
+ const withdrawn = withdrawProposal(ledger, now);
76
+ await deps.persist(withdrawn);
77
+ return reply(withdrawn, 'Sure. What would you like to change?');
78
+ }
79
+ return collect(ledger, text, deps);
80
+ }
81
+ case 'awaiting_consent': {
82
+ if (button !== undefined)
83
+ return IGNORED;
84
+ const decision = await consentReply(text, spec, deps.model);
85
+ if (decision === 'unclear')
86
+ return reply(ledger, 'Just to be clear, is that a yes or a no?');
87
+ const next = applyConsent(spec, ledger, decision, consentVersion(spec), now);
88
+ if (decision === 'declined') {
89
+ await deps.persist(next);
90
+ return reply(next, 'Understood. I have not sent anything.');
91
+ }
92
+ return propose(next, deps);
93
+ }
94
+ default:
95
+ return button === undefined ? collect(ledger, text, deps) : IGNORED;
96
+ }
97
+ }
98
+ /** Load, persist and clear the participant's ledger inside the binding's own transaction scope. */
99
+ export function collectionCustody(store, bindingId, participantId) {
100
+ return {
101
+ load: () => store.transaction([bindingId], (tx) => loadCollection(tx, bindingId, participantId)),
102
+ persist: (ledger) => store.transaction([bindingId], (tx) => ledger.phase === 'cancelled' || ledger.phase === 'expired'
103
+ ? deleteCollection(tx, bindingId, participantId)
104
+ : saveCollection(tx, ledger)),
105
+ remove: () => store.transaction([bindingId], (tx) => deleteCollection(tx, bindingId, participantId)),
106
+ };
107
+ }
108
+ /** The trusted messaging admission bucket: binding-scoped, derived after webhook authentication. */
109
+ export function messagingAdmissionDigest(bindingId, participantId) {
110
+ return channelDigest(JSON.stringify(['messaging', bindingId, participantId]));
111
+ }
112
+ /**
113
+ * Remove marked-private values from text bound for the conversation model or the transcript: every
114
+ * candidate of a private typed control, plus literal occurrences of a captured private text value.
115
+ */
116
+ export function redactPrivate(spec, ledger, text) {
117
+ const privates = spec.fields.filter((field) => field.private === true);
118
+ const controls = [...new Set(privates.map((field) => field.control).filter(isParsed))];
119
+ let out = redactSpans(text, scanTypedValues(text, controls), (span) => `[${privates.find((field) => field.control === span.control)?.title ?? span.control} withheld]`);
120
+ for (const field of privates) {
121
+ if (isParsed(field.control))
122
+ continue;
123
+ const value = ledger.values[field.key];
124
+ if (typeof value !== 'string' || value.trim().length < 2)
125
+ continue;
126
+ out = out.replace(new RegExp(value.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), `[${field.title} withheld]`);
127
+ }
128
+ return out;
129
+ }
130
+ async function collect(ledger, text, deps) {
131
+ const { spec, now } = deps;
132
+ const captured = await capture(spec, reopenCorrected(spec, ledger, text, now), text, deps.model, now);
133
+ if (captured.phase === 'awaiting_consent') {
134
+ await deps.persist(captured);
135
+ return reply(captured, spec.consentQuestion);
136
+ }
137
+ if (captured.phase === 'awaiting_confirmation')
138
+ return propose(captured, deps);
139
+ await deps.persist(captured);
140
+ return {
141
+ kind: 'model',
142
+ ledger: captured,
143
+ utterance: redactPrivate(spec, captured, text),
144
+ note: modelNote(spec, captured, now),
145
+ };
146
+ }
147
+ /**
148
+ * A captured field named beside a correction cue, or a fresh value of a typed control that has one
149
+ * field, is a correction: reopen it so capture can apply the change and the proposal is invalidated.
150
+ */
151
+ function reopenCorrected(spec, ledger, text, now) {
152
+ const lower = text.toLowerCase();
153
+ const cue = CORRECTION_CUES.test(lower);
154
+ let next = ledger;
155
+ for (const field of spec.fields) {
156
+ if (field.control === 'consent' || next.fields[field.key]?.status !== 'captured')
157
+ continue;
158
+ const named = lower.includes(field.title.toLowerCase()) || new RegExp(`\\b${field.key}\\b`, 'i').test(text);
159
+ const typed = isParsed(field.control) &&
160
+ spec.fields.filter((candidate) => candidate.control === field.control).length === 1 &&
161
+ scanTypedValues(text, [field.control]).length > 0;
162
+ if ((named && cue) || typed)
163
+ next = rejectField(spec, next, field.key, now);
164
+ }
165
+ return next;
166
+ }
167
+ async function propose(ledger, deps) {
168
+ const { spec, now } = deps;
169
+ const prepared = await deps.prepare(spec.action, actionInput(spec, deps.artifact, ledger));
170
+ if (prepared.status !== 'confirmation_required') {
171
+ const cancelled = cancelCollection(ledger, now);
172
+ await deps.persist(cancelled);
173
+ return reply(cancelled, `I could not prepare that for sending, so nothing was saved. For help, contact ${deps.supportEmail}.`);
174
+ }
175
+ const review = renderReview(spec, ledger.values, ledger.consent);
176
+ const id = randomUUID();
177
+ const proposed = attachProposal(spec, ledger, { id, review, continuation: prepared.continuation }, now);
178
+ await deps.persist(proposed);
179
+ const buttons = deps.buttons?.render(id);
180
+ return {
181
+ kind: 'reply',
182
+ ledger: proposed,
183
+ reply: `${review}\n\nShall I send it?`,
184
+ ...(buttons === undefined ? {} : { buttons }),
185
+ };
186
+ }
187
+ async function execute(ledger, deps) {
188
+ const { spec, now } = deps;
189
+ const proposal = ledger.proposal;
190
+ if (proposal === undefined) {
191
+ const cancelled = cancelCollection(ledger, now);
192
+ await deps.persist(cancelled);
193
+ return reply(cancelled, 'That review is no longer available, so nothing was sent.');
194
+ }
195
+ // The durable claim precedes the external effect; a retry re-enters here with the same identity.
196
+ await deps.persist(ledger);
197
+ const result = await deps.execute(proposal.id, proposal.continuation);
198
+ if (result.status === 'completed') {
199
+ const done = completeCollection(ledger, now);
200
+ await deps.persist(done);
201
+ return reply(done, spec.successMessage);
202
+ }
203
+ const cancelled = cancelCollection(ledger, now);
204
+ await deps.persist(cancelled);
205
+ const changed = result.status === 'failed' && result.error.code === 'configuration_changed';
206
+ return reply(cancelled, `${changed ? 'The application changed while you were reviewing, so ' : ''}I could not send that, and nothing was saved. Please try again later or contact ${deps.supportEmail}.`);
207
+ }
208
+ /** The prepared action's input: every ledger value, plus the empty value for an optional field the schema still requires. */
209
+ function actionInput(spec, artifact, ledger) {
210
+ const action = artifact.tools.find((tool) => tool.name === spec.action);
211
+ const required = new Set(Array.isArray(action?.inputSchema.required)
212
+ ? action.inputSchema.required.filter((key) => typeof key === 'string')
213
+ : []);
214
+ const input = { ...ledger.values };
215
+ for (const field of spec.fields)
216
+ if (field.optional === true && !(field.key in input) && required.has(field.key))
217
+ input[field.key] = '';
218
+ return input;
219
+ }
220
+ function consentVersion(spec) {
221
+ return channelDigest(spec.consentQuestion).slice(0, 16);
222
+ }
223
+ function modelNote(spec, ledger, now) {
224
+ const optional = spec.fields.filter((field) => field.optional === true).map((field) => field.key);
225
+ return [
226
+ `Natural collection is open for "${spec.interactionId}": the platform validates values, asks the consent question, shows the review and confirms; you only converse.`,
227
+ `Ledger: ${JSON.stringify({ ...ledgerForModel(spec, ledger, now), optional })}`,
228
+ 'Ask naturally, in any grouping and without counters, only for fields whose status is missing, invalid, ambiguous or rejected_by_user; optional fields may stay blank. Answer side questions normally.',
229
+ 'Never ask for or repeat a private value, never mention a form, never claim anything was sent, and never ask a yes-or-no question while a proposal is pending.',
230
+ ].join('\n');
231
+ }
232
+ //# sourceMappingURL=collection-turn.js.map
@@ -18,6 +18,7 @@ export function createWhatsAppRuntime(options, deps, history) {
18
18
  return undefined;
19
19
  return new WhatsAppRuntime({
20
20
  ...options,
21
+ ...(history.installations ? { installations: history.installations } : {}),
21
22
  ...(history.allowance
22
23
  ? {
23
24
  historyDays: async (tenant) => {
@@ -0,0 +1,45 @@
1
+ import { isNativeRecordsOperation, } from '@noodle-borg/compiler';
2
+ /**
3
+ * Runtime twin of the compiler's messaging requirement check (ADR 0240). A binding selects a subset
4
+ * of the authored surface, so the projected artifact is re-checked with the compiler's own
5
+ * native-records predicate: pure reads and knowledge pass as before; a `collect` opener passes with
6
+ * its confirmed native-record action selected beside it; everything else names, as a channel error
7
+ * code, the requirement the messaging profile lacks.
8
+ */
9
+ export function messagingProjectionIneligibility(artifact) {
10
+ const interactions = artifact.toolInteractions ?? {};
11
+ const present = new Set(artifact.tools.map((tool) => tool.name));
12
+ const actions = new Set(Object.entries(interactions)
13
+ .filter(([opener]) => present.has(opener))
14
+ .map(([, interaction]) => interaction.action));
15
+ for (const tool of artifact.tools) {
16
+ const interaction = interactions[tool.name];
17
+ if (interaction !== undefined && !present.has(interaction.action))
18
+ return 'channel_dependency_missing';
19
+ const operations = operationRefs(tool.fulfilment);
20
+ if (actions.has(tool.name)) {
21
+ const [operation] = operations;
22
+ if (operations.length !== 1 || operation === undefined)
23
+ return 'messaging_action_unsupported';
24
+ if (!isNativeRecordsOperation(operation, undefined))
25
+ return 'external_lookup_not_enabled';
26
+ continue;
27
+ }
28
+ if (tool.annotations?.readOnlyHint !== true ||
29
+ (tool._meta?.ui !== undefined && interaction === undefined))
30
+ return 'messaging_action_unsupported';
31
+ // No separately budgeted connector-backed inference yet: external lookups need their own slice.
32
+ if (operations.length > 0)
33
+ return 'external_lookup_not_enabled';
34
+ }
35
+ const ambient = artifact.server.context?.ambient;
36
+ if (ambient !== undefined && operationRefs(ambient.fulfilment).length > 0)
37
+ return 'external_lookup_not_enabled';
38
+ return undefined;
39
+ }
40
+ function operationRefs(fulfilment) {
41
+ return fulfilment.kind === 'operation'
42
+ ? [fulfilment.operationRef]
43
+ : fulfilment.steps.flatMap((step) => (step.kind === 'operation' ? [step.operationRef] : []));
44
+ }
45
+ //# sourceMappingURL=messaging-eligibility.js.map
@@ -0,0 +1,23 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ const ACTS = ['confirm', 'edit', 'cancel'];
3
+ export const REVIEW_BUTTON_TITLES = {
4
+ confirm: 'Confirm',
5
+ edit: 'Edit',
6
+ cancel: 'Cancel',
7
+ };
8
+ export function reviewButtonBinding(indexKey, bindingId, participantId) {
9
+ const id = (proposalId, act) => `b_${createHmac('sha256', Buffer.from(indexKey, 'base64'))
10
+ .update(JSON.stringify(['review-button', bindingId, participantId, proposalId, act]))
11
+ .digest('base64url')}`;
12
+ return {
13
+ render: (proposalId) => ACTS.map((act) => ({ id: id(proposalId, act), title: REVIEW_BUTTON_TITLES[act] })),
14
+ act: (proposalId, buttonId) => {
15
+ const tapped = Buffer.from(buttonId);
16
+ return ACTS.find((act) => {
17
+ const expected = Buffer.from(id(proposalId, act));
18
+ return expected.length === tapped.length && timingSafeEqual(expected, tapped);
19
+ });
20
+ },
21
+ };
22
+ }
23
+ //# sourceMappingURL=reply-buttons.js.map