@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
@@ -1,11 +1,17 @@
1
- import { beginChannelProviderBlock, CHANNEL_RETENTION_MS, ChannelCoordinator, ChannelError, channelDigest, channelProviderBlocks, finishChannelProviderBlock, messagingSurfaceOf, projectArtifactForSurface, resolveInvocationContextSnapshot, withAssistantTurnExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
1
+ import { beginChannelProviderBlock, CHANNEL_RETENTION_MS, ChannelCoordinator, ChannelError, channelDigest, channelProviderBlocks, collectionSpecFor, finishChannelProviderBlock, messagingSurfaceOf, projectArtifactForSurface, resolveInvocationContextSnapshot, WHATSAPP_CONFIRMATION_EXPIRY_MS, withAssistantTurnExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
2
+ import { guardedFetch } from '@noodle-borg/connector-http';
3
+ import { executePreparedTool, prepareToolForConfirmation } from '@noodle-borg/runtime';
2
4
  import { runAgentTurn } from '../routes/assistant-agent.js';
3
5
  import { resolveAssistantKnowledge } from '../routes/assistant-knowledge.js';
4
6
  import { resolveAssistantModelBinding } from '../routes/assistant-model-binding.js';
5
7
  import { activeAssistantTarget } from '../routes/assistant-session-target.js';
6
8
  import { resolveConfigScope } from '../store.js';
7
9
  import { Dialog360, verifyWhatsAppWebhookSecret, WHATSAPP_CALLBACK_HEADER } from './360dialog.js';
10
+ import { capabilityReport } from './capability-report.js';
11
+ import { collectionCustody, messagingAdmissionDigest, redactPrivate, runCollectionTurn, } from './collection-turn.js';
8
12
  import { verifiedInferenceBound, withChannelInferenceGuard } from './inference-guard.js';
13
+ import { messagingProjectionIneligibility } from './messaging-eligibility.js';
14
+ import { reviewButtonBinding } from './reply-buttons.js';
9
15
  export class WhatsAppRuntime {
10
16
  options;
11
17
  deps;
@@ -41,14 +47,9 @@ export class WhatsAppRuntime {
41
47
  if (binding.capabilities.some((cap) => !surface.capabilities.some((authored) => authored.kind === cap.kind && authored.name === cap.name)))
42
48
  throw new ChannelError('capability_not_authored');
43
49
  const artifact = projectArtifactForSurface(target.served.artifact, binding.capabilities);
44
- if (artifact.tools.some((tool) => tool.annotations?.readOnlyHint !== true || tool._meta?.ui))
45
- throw new ChannelError('messaging_action_unsupported');
46
- // This first slice has no separately budgeted connector-backed inference. Pure reads and
47
- // the existing corpus search remain available; external lookups require their owning slice.
48
- const external = (fulfilment) => fulfilment.kind === 'operation' || fulfilment.steps.some((step) => step.kind === 'operation');
49
- if (artifact.tools.some((tool) => external(tool.fulfilment)) ||
50
- (artifact.server.context?.ambient && external(artifact.server.context.ambient.fulfilment)))
51
- throw new ChannelError('external_lookup_not_enabled');
50
+ const ineligible = messagingProjectionIneligibility(artifact);
51
+ if (ineligible !== undefined)
52
+ throw new ChannelError(ineligible);
52
53
  return { ...target, served: { ...target.served, artifact } };
53
54
  }
54
55
  async doctor(id, req) {
@@ -75,8 +76,9 @@ export class WhatsAppRuntime {
75
76
  if (!this.options.worker.ready())
76
77
  throw new ChannelError('worker_unavailable');
77
78
  });
79
+ let target;
78
80
  await check('deployment', async () => {
79
- await this.target(binding);
81
+ target = await this.target(binding);
80
82
  });
81
83
  await check('model_cost', async () => {
82
84
  const model = await resolveAssistantModelBinding(await this.target(binding), binding.tenant, binding.deploymentId, this.deps, 'whatsapp');
@@ -109,7 +111,20 @@ export class WhatsAppRuntime {
109
111
  const ready = checks.every((check) => check.status === 'ready');
110
112
  if (ready)
111
113
  await this.channels.markReady(id, binding.revision);
112
- return { ready, revision: binding.revision, checks, webhookUrl: this.webhookUrl(req, binding) };
114
+ // The compatibility report rides beside readiness and never changes it (ADR 0240 decision 7).
115
+ const capabilities = await capabilityReport({
116
+ artifact: target?.served.artifact,
117
+ tenant: binding.tenant,
118
+ durable: this.options.store.durable,
119
+ installations: this.options.installations,
120
+ });
121
+ return {
122
+ ready,
123
+ revision: binding.revision,
124
+ checks,
125
+ capabilities,
126
+ webhookUrl: this.webhookUrl(req, binding),
127
+ };
113
128
  }
114
129
  async webhook(id, req) {
115
130
  const binding = await this.channels.internal(id);
@@ -247,7 +262,11 @@ export class WhatsAppRuntime {
247
262
  const { adapter } = await this.provider(send.binding);
248
263
  await this.channels.assertSend(id, send.event.id, send.event.lease);
249
264
  this.options.worker.signal.throwIfAborted();
250
- await this.channels.sent(id, send.event.id, send.event.lease, await adapter.send({ to: send.participant.address, text: send.event.reply }));
265
+ await this.channels.sent(id, send.event.id, send.event.lease, await adapter.send({
266
+ to: send.participant.address,
267
+ text: send.event.reply,
268
+ ...(send.event.buttons === undefined ? {} : { buttons: send.event.buttons }),
269
+ }));
251
270
  }
252
271
  }
253
272
  catch {
@@ -277,9 +296,11 @@ export class WhatsAppRuntime {
277
296
  timer.unref?.();
278
297
  try {
279
298
  const target = await this.target(binding);
299
+ const artifact = target.served.artifact;
280
300
  const model = await resolveAssistantModelBinding(target, binding.tenant, binding.deploymentId, this.deps, 'whatsapp');
281
301
  if (!model)
282
302
  throw new ChannelError('model_not_configured');
303
+ const guarded = withChannelInferenceGuard(model, this.channels, binding.id, event.id, beforeStep);
283
304
  const session = {
284
305
  kind: 'messaging',
285
306
  channel: 'whatsapp',
@@ -292,34 +313,107 @@ export class WhatsAppRuntime {
292
313
  history: participant.history.map(({ role, content }) => ({ role, content })),
293
314
  modelToolUses: participant.modelToolUses ?? [],
294
315
  };
316
+ const executeDeps = withAssistantTurnExecutionAuthority({ ...target.served.deps, signal }, artifact, session);
295
317
  const context = await resolveInvocationContextSnapshot({
296
- artifact: target.served.artifact,
297
- executeDeps: withAssistantTurnExecutionAuthority({ ...target.served.deps, signal }, target.served.artifact, session),
318
+ artifact,
319
+ executeDeps,
298
320
  caller: session.caller,
299
321
  instant: new Date(this.channels.now()),
300
322
  });
301
- let text = '', failure;
302
- await runAgentTurn(target, session, event.text, context, this.deps, (result) => {
303
- if (result.event === 'content' && typeof result.data.delta === 'string')
304
- text += result.data.delta;
305
- if (result.event === 'error')
306
- failure = String(result.data.code ?? 'answer_failed');
307
- }, undefined, undefined, undefined, false, {
308
- binding: withChannelInferenceGuard(model, this.channels, binding.id, event.id, beforeStep),
309
- beforeStep,
310
- claimTool: (name) => this.channels.claimTool(binding.id, participant.id, name),
311
- signal,
312
- });
313
- if (failure || !text.trim())
314
- throw new ChannelError(failure ?? 'answer_unavailable');
315
- const disclosure = participant.history.length === 0
316
- ? `I’m ${target.served.artifact.server.branding?.name ?? target.served.artifact.server.title}’s AI assistant.\n\n`
317
- : '';
318
- const answer = disclosure + text.trim();
323
+ const now = this.channels.now();
324
+ const inbound = event.text ?? '';
325
+ // A tapped button is input only for the pending review: it never reaches the model as text,
326
+ // and history keeps the label the participant saw.
327
+ const button = event.button;
328
+ const heard = event.text ?? button?.title ?? '';
329
+ const custody = collectionCustody(this.options.store, binding.id, participant.id);
330
+ let ledger = await custody.load();
331
+ let spec = ledger === undefined
332
+ ? undefined
333
+ : collectionSpecFor(artifact, ledger.interactionId, WHATSAPP_CONFIRMATION_EXPIRY_MS);
334
+ if (ledger !== undefined && spec === undefined) {
335
+ // The deployment no longer declares this interaction: the stale ledger is dropped.
336
+ await custody.remove();
337
+ ledger = undefined;
338
+ }
339
+ let outcome = button === undefined ? { kind: 'none' } : { kind: 'ignored', code: 'button_unbound' };
340
+ if (ledger !== undefined && spec !== undefined) {
341
+ const toolDeps = { ...executeDeps, caller: session.caller, context };
342
+ outcome = await runCollectionTurn(ledger, inbound, {
343
+ spec,
344
+ artifact,
345
+ now,
346
+ supportEmail: binding.supportEmail,
347
+ buttons: reviewButtonBinding(binding.indexKey, binding.id, participant.id),
348
+ model: {
349
+ binding: guarded,
350
+ fetcher: this.deps.modelFetch ?? ((url, init) => guardedFetch(new URL(url), init)),
351
+ signal,
352
+ },
353
+ prepare: (action, input) => prepareToolForConfirmation(artifact, action, input, toolDeps),
354
+ execute: async (proposalId, continuation) => {
355
+ await beforeStep();
356
+ return executePreparedTool(artifact, continuation, {
357
+ ...toolDeps,
358
+ invocationId: proposalId,
359
+ publicAdmission: {
360
+ messaging: messagingAdmissionDigest(binding.id, participant.id),
361
+ provenance: { kind: 'messaging', channel: 'whatsapp' },
362
+ },
363
+ });
364
+ },
365
+ persist: custody.persist,
366
+ remove: custody.remove,
367
+ }, button?.id);
368
+ ledger = outcome.kind === 'reply' || outcome.kind === 'model' ? outcome.ledger : undefined;
369
+ }
370
+ if (outcome.kind === 'ignored') {
371
+ // An unknown, stale or foreign token: no reply, no model call, one content-free code.
372
+ await this.channels.fail(binding.id, event.id, event.lease, outcome.code, 'cancelled');
373
+ return;
374
+ }
375
+ let answer;
376
+ if (outcome.kind === 'reply')
377
+ answer = outcome.reply;
378
+ else {
379
+ let text = '', failure;
380
+ await runAgentTurn(target, session, outcome.kind === 'model' ? outcome.utterance : inbound, context, this.deps, (result) => {
381
+ if (result.event === 'content' && typeof result.data.delta === 'string')
382
+ text += result.data.delta;
383
+ if (result.event === 'error')
384
+ failure = String(result.data.code ?? 'answer_failed');
385
+ }, undefined, undefined, undefined, false, {
386
+ binding: guarded,
387
+ beforeStep,
388
+ claimTool: (name) => this.channels.claimTool(binding.id, participant.id, name),
389
+ signal,
390
+ collection: {
391
+ open: ledger,
392
+ now,
393
+ retentionMs: historyRetention,
394
+ confirmationExpiryMs: WHATSAPP_CONFIRMATION_EXPIRY_MS,
395
+ save: async (opened) => {
396
+ await custody.persist(opened);
397
+ ledger = opened;
398
+ spec = collectionSpecFor(artifact, opened.interactionId, WHATSAPP_CONFIRMATION_EXPIRY_MS);
399
+ },
400
+ },
401
+ ...(outcome.kind === 'model' ? { systemNote: outcome.note } : {}),
402
+ });
403
+ if (failure || !text.trim())
404
+ throw new ChannelError(failure ?? 'answer_unavailable');
405
+ const disclosure = participant.history.length === 0
406
+ ? `I’m ${artifact.server.branding?.name ?? artifact.server.title}’s AI assistant.\n\n`
407
+ : '';
408
+ answer = disclosure + text.trim();
409
+ }
319
410
  const bounded = answer.length <= 3500
320
411
  ? answer
321
412
  : `${answer.slice(0, 3350).replace(/[\uD800-\uDBFF]$/u, '')}\n\nAsk me to expand on any part.`;
322
- await this.channels.complete(binding.id, event.id, event.lease, bounded, historyRetention);
413
+ // Marked-private values never persist: history and the event row keep redacted text, and the
414
+ // full review outlives only its own delivery.
415
+ const scrub = (text) => ledger !== undefined && spec !== undefined ? redactPrivate(spec, ledger, text) : text;
416
+ await this.channels.complete(binding.id, event.id, event.lease, bounded, historyRetention, undefined, { user: scrub(heard), assistant: scrub(bounded) }, outcome.kind === 'reply' ? outcome.buttons : undefined);
323
417
  }
324
418
  catch (error) {
325
419
  const code = error instanceof ChannelError ? error.code : 'answer_failed';
@@ -98,7 +98,7 @@ class NativeRecordConnector {
98
98
  idempotencyKey: key,
99
99
  payload,
100
100
  ...(publicInput ? { publicInput: true } : {}),
101
- origin: { kind: 'mcp', reference: 'native-tool' },
101
+ origin: trustedOrigin(call),
102
102
  actorSubject,
103
103
  };
104
104
  const completed = await store.probeRequest(request);
@@ -113,8 +113,10 @@ class NativeRecordConnector {
113
113
  if (publicInput) {
114
114
  if (!installation.intakeActive || this.dependencies.publicIntakeEnabled === false)
115
115
  return fail(call, 503, 'Public record intake is paused.');
116
- if (call.publicAdmission === undefined || !call.publicAdmission.network)
117
- return fail(call, 503, 'Trusted network admission context is required for public record writes.');
116
+ if (call.publicAdmission === undefined ||
117
+ (!call.publicAdmission.network && !call.publicAdmission.messaging))
118
+ return fail(call, 503, 'Trusted network or messaging admission context is required for public record writes.');
119
+ const { provenance: _provenance, ...buckets } = call.publicAdmission;
118
120
  const quota = await admitPublicRecord({
119
121
  counters: this.dependencies.counters,
120
122
  surfaceId: installation.publicId,
@@ -122,7 +124,7 @@ class NativeRecordConnector {
122
124
  key: `solution-intake:logical:${installation.publicId}:${idempotencyDigest(key)}`,
123
125
  fingerprint: requestFingerprint(request),
124
126
  },
125
- buckets: call.publicAdmission,
127
+ buckets,
126
128
  now: this.dependencies.now?.() ?? new Date(),
127
129
  });
128
130
  if (!quota.allowed)
@@ -196,6 +198,19 @@ class NativeRecordConnector {
196
198
  : { ok: true, record: recordToWire(result.record) };
197
199
  }
198
200
  }
201
+ /**
202
+ * Record provenance comes only from the trusted call context (ADR 0240): a messaging channel names
203
+ * itself through `publicAdmission.provenance` and must arrive with its participant bucket; anything
204
+ * a caller claims inside tool input has no effect.
205
+ */
206
+ function trustedOrigin(call) {
207
+ const provenance = call.publicAdmission?.provenance;
208
+ if (provenance === undefined)
209
+ return { kind: 'mcp', reference: 'native-tool' };
210
+ if (!call.publicAdmission?.messaging)
211
+ return fail(call, 503, 'Messaging provenance requires a trusted participant admission bucket.');
212
+ return { kind: 'messaging', reference: provenance.channel };
213
+ }
199
214
  function receipt(record) {
200
215
  return { ok: true, recordId: record.id, revision: record.revision };
201
216
  }
@@ -1,6 +1,6 @@
1
1
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
2
- import { APPLICATION_DRAFT_LIMITS, ApplicationDraftCreateRequestSchema, ApplicationDraftDiffRequestSchema, ApplicationDraftDiffResponseSchema, ApplicationDraftEditRequestSchema, ApplicationDraftHistoryResponseSchema, ApplicationDraftListResponseSchema, ApplicationDraftResponseSchema, ApplicationDraftRevisionRequestSchema, ApplicationDraftUndoRequestSchema, } from '@noodle-borg/wire-contracts';
3
- import { ApplicationDraftError } from '../application-drafts/contracts.js';
2
+ import { APPLICATION_DRAFT_LIMITS, ApplicationDraftCreateRequestSchema, ApplicationDraftDiffRequestSchema, ApplicationDraftDiffResponseSchema, ApplicationDraftEditRequestSchema, ApplicationDraftHistoryResponseSchema, ApplicationDraftListResponseSchema, ApplicationDraftResponseSchema, ApplicationDraftRevisionRequestSchema, ApplicationDraftUndoRequestSchema, ApplicationDraftValidationResponseSchema, } from '@noodle-borg/wire-contracts';
3
+ import { ApplicationDraftError, DraftValidationUnavailableError, } from '../application-drafts/contracts.js';
4
4
  import { applicationDraftMethods, parseApplicationDraftPath } from '../application-drafts/paths.js';
5
5
  import { admitBusinessTarget, authorizeBusinessApi } from '../business-api-admission.js';
6
6
  import { requestIdempotencyKey } from './business-information-request.js';
@@ -11,6 +11,14 @@ export async function handleApplicationDraftRoute(req, res, url, deps) {
11
11
  await handle(req, res, url, deps);
12
12
  }
13
13
  catch (error) {
14
+ if (error instanceof DraftValidationUnavailableError) {
15
+ if (error.code === 'busy')
16
+ res.setHeader('retry-after', '2');
17
+ return sendJson(res, error.code === 'busy' ? 429 : 503, {
18
+ code: error.code === 'busy' ? 'validation_busy' : 'validation_unavailable',
19
+ error: 'Draft validation is temporarily unavailable. Nothing was published.',
20
+ });
21
+ }
14
22
  if (error instanceof ApplicationDraftError) {
15
23
  const status = error.code === 'forbidden'
16
24
  ? 403
@@ -94,6 +102,40 @@ async function handle(req, res, url, deps) {
94
102
  const body = await readJsonBody(req, Math.min(deps.maxBody, APPLICATION_DRAFT_LIMITS.totalBytes * 6 + 32_768));
95
103
  if (!body.ok)
96
104
  return sendJson(res, body.status, { error: body.error });
105
+ if (id && action === 'validate' && req.method === 'POST') {
106
+ const parsed = ApplicationDraftRevisionRequestSchema.safeParse(body.value);
107
+ if (!parsed.success)
108
+ return invalid(res);
109
+ if (!deps.compiler)
110
+ return sendJson(res, 503, {
111
+ code: 'validation_unavailable',
112
+ error: 'Draft validation is not configured. Nothing was published.',
113
+ });
114
+ const input = { scope, id, actorSubject, expectedRevision: parsed.data.expectedRevision };
115
+ const draft = await deps.drafts.forValidation(input);
116
+ const result = await deps.compiler.compile(draft.source);
117
+ // Do not hold a database lock during customer compilation. Recheck under the authority lock afterwards.
118
+ const current = await deps.drafts.forValidation(input);
119
+ if (current.sourceDigest !== draft.sourceDigest ||
120
+ (result.ok && result.sourceDigest !== draft.sourceDigest))
121
+ throw new ApplicationDraftError('revision_conflict', current.revision);
122
+ const validation = {
123
+ draftId: id,
124
+ revision: draft.revision,
125
+ sourceDigest: draft.sourceDigest,
126
+ check: 'source-and-manifest',
127
+ published: false,
128
+ ...(result.ok
129
+ ? {
130
+ status: 'valid',
131
+ compilerDigest: result.compilerDigest,
132
+ artifactDigest: result.artifactDigest,
133
+ issues: [],
134
+ }
135
+ : { status: 'invalid', issues: result.issues }),
136
+ };
137
+ return sendJson(res, 200, ApplicationDraftValidationResponseSchema.parse({ ok: true, data: { validation } }));
138
+ }
97
139
  if (id !== undefined && req.method === 'DELETE') {
98
140
  const parsed = ApplicationDraftRevisionRequestSchema.safeParse(body.value);
99
141
  if (!parsed.success)
@@ -94,6 +94,9 @@ export async function runAgentTurn(target, session, message, context, deps, emit
94
94
  ...(pageContext === undefined ? {} : { pageContext }),
95
95
  ...(modelContext === undefined ? {} : { modelContext }),
96
96
  }),
97
+ ...(transport?.systemNote === undefined
98
+ ? []
99
+ : [{ role: 'system', content: transport.systemNote }]),
97
100
  ...session.history,
98
101
  { role: 'user', content: message },
99
102
  ];
@@ -272,6 +275,7 @@ export async function runAgentTurn(target, session, message, context, deps, emit
272
275
  context,
273
276
  session,
274
277
  claimTool: transport?.claimTool ?? (async () => false),
278
+ ...(transport?.collection === undefined ? {} : { collection: transport.collection }),
275
279
  })
276
280
  : await dispatchAssistantTool({
277
281
  artifact: target.served.artifact,
@@ -0,0 +1,23 @@
1
+ import { applySecurityHeaders, enforceHttps } from '@noodle-borg/transport-http';
2
+ import { parseApplicationDraftPath } from '../application-drafts/paths.js';
3
+ import { parseBusinessWorkspacePath } from '../business-workspaces/paths.js';
4
+ import { handleApplicationDraftRoute, } from './application-drafts.js';
5
+ import { handleBusinessWorkspaceRoute } from './business-workspaces.js';
6
+ /** Explicit versioned authority, independent of the managed-record switch. */
7
+ export function dispatchBusinessAuthoringRoutes(req, res, url, authoring, deps) {
8
+ if (!authoring)
9
+ return false;
10
+ const workspace = url.pathname === '/v1/me/business-workspaces' || parseBusinessWorkspacePath(url.pathname);
11
+ if (!workspace && !parseApplicationDraftPath(url.pathname))
12
+ return false;
13
+ applySecurityHeaders(res, deps.tls);
14
+ if (!enforceHttps(req, res, deps.tls)) {
15
+ const merged = { ...deps, ...authoring };
16
+ if (workspace)
17
+ void handleBusinessWorkspaceRoute(req, res, url, merged);
18
+ else
19
+ void handleApplicationDraftRoute(req, res, url, merged);
20
+ }
21
+ return true;
22
+ }
23
+ //# sourceMappingURL=business-authoring-dispatch.js.map
@@ -0,0 +1,39 @@
1
+ import { sendJson } from '@noodle-borg/transport-http';
2
+ import { admitBusinessTarget, authorizeBusinessApi } from '../business-api-admission.js';
3
+ import { businessGrantAllows, } from '../business-information/portable.js';
4
+ import { sendForbidden } from '../http-util.js';
5
+ export async function requireIdentity(req, res, deps) {
6
+ return authorizeBusinessApi(req, res, deps);
7
+ }
8
+ export async function requireInstallationGrant(res, ref, identity, deps, permission) {
9
+ if (ref.installationId === undefined)
10
+ return undefined;
11
+ const installation = await deps.store.getInstallationById(ref.org, ref.installationId);
12
+ if (installation === undefined) {
13
+ sendJson(res, 404, { error: 'installation not found' });
14
+ return undefined;
15
+ }
16
+ const grant = await resolveBusinessStaffGrant(deps, installation.scope, identity.subject);
17
+ if (grant?.revokedAt !== undefined || grant === undefined) {
18
+ sendForbidden(res, 'business grant required');
19
+ return undefined;
20
+ }
21
+ if (permission && !businessGrantAllows(grant, permission)) {
22
+ sendForbidden(res, `business permission required: ${permission}`);
23
+ return undefined;
24
+ }
25
+ if (!(await admitBusinessTarget(res, installation.scope)))
26
+ return undefined;
27
+ return { installation, scope: installation.scope, grant };
28
+ }
29
+ export async function resolveBusinessStaffGrant(deps, scope, subject) {
30
+ return deps.store.staff.resolve(scope, subject);
31
+ }
32
+ export async function requireInstallationPermission(res, ref, identity, permission, deps) {
33
+ return requireInstallationGrant(res, ref, identity, deps, permission);
34
+ }
35
+ /** Local store effects only: respond after the authority transaction commits. */
36
+ export async function runBusinessStaffOperation(deps, scope, actor, permission, operation) {
37
+ return deps.store.staff.run(scope, actor, permission, operation);
38
+ }
39
+ //# sourceMappingURL=business-information-access.js.map
@@ -57,7 +57,7 @@ export async function handleApplicationActivity(req, res, url, ref, deps) {
57
57
  body,
58
58
  reviewer: identity.subject,
59
59
  canEdit: businessGrantAllows(authorized.grant, 'installation:administer'),
60
- });
60
+ }, (operation) => deps.store.staff.run(authorized.scope, identity.subject, permission, operation));
61
61
  if ('audit' in result)
62
62
  await deps.audit?.emit({
63
63
  ...result.audit,
@@ -37,12 +37,12 @@ export async function handleBusinessChannels(req, res, ref, deps) {
37
37
  code: 'business_setup_required',
38
38
  error: 'Complete business setup before publishing agent channels.',
39
39
  });
40
- const result = await deps.store.setIntakeState({
40
+ const result = await deps.store.staff.run(authorized.scope, identity.subject, 'installation:administer', () => deps.store.setIntakeState({
41
41
  scope: authorized.scope,
42
42
  expectedRevision: parsed.data.expectedRevision,
43
43
  active: parsed.data.active,
44
44
  actorSubject: identity.subject,
45
- });
45
+ }));
46
46
  if (!result.ok)
47
47
  return mutationFailure(res, result);
48
48
  installation = result.installation;
@@ -26,10 +26,16 @@ export async function handleApplicationConnections(req, res, ref, deps) {
26
26
  try {
27
27
  const targets = await deps.resolveConnectionTargets(authorized.installation);
28
28
  const connections = deps.connections;
29
- const projection = async () => ({
30
- connections: await Promise.all(targets.map((target) => connections.inspect(target))),
31
- canEdit: businessGrantAllows(authorized.grant, 'installation:administer'),
32
- });
29
+ const projection = async () => {
30
+ const views = await Promise.all(targets.map((target) => connections.inspect(target, (operation) => deps.store.staff.run(authorized.scope, identity.subject, 'records:read', operation, () => new ConnectionError('connection_denied')))));
31
+ const current = await deps.store.staff.resolve(authorized.scope, identity.subject);
32
+ if (!businessGrantAllows(current, 'records:read') || current?.role === 'viewer')
33
+ throw new ConnectionError('connection_denied');
34
+ return {
35
+ connections: views,
36
+ canEdit: businessGrantAllows(current, 'installation:administer'),
37
+ };
38
+ };
33
39
  if (!mutation)
34
40
  return sendJson(res, 200, ApplicationConnectionsResponseSchema.parse({ ok: true, data: await projection() }));
35
41
  const target = targets.find((item) => item.key.connectionId === ref.connectionId);
@@ -1,14 +1,13 @@
1
1
  import { OrganizationAgreementError } from '@noodle-borg/control-plane/portable';
2
2
  import { sendJson } from '@noodle-borg/transport-http';
3
- import { parseApplicationDraftPath } from '../application-drafts/paths.js';
4
3
  import { BusinessNoticeError } from '../business-information/business-notice.js';
5
4
  import { parseBusinessPagePath } from '../business-information/business-page-paths.js';
6
5
  import { InstallationCapacityError } from '../business-information/installation-capacity.js';
7
6
  import { NativeStorageLimitError } from '../business-information/native-storage-budget.js';
8
7
  import { SourceCredentialError } from '../business-information/source-credential-fence.js';
9
8
  import { SourceCapacityError } from '../business-information/source-custody-budget.js';
9
+ import { BusinessWorkspaceError } from '../business-workspaces/contracts.js';
10
10
  import { respondRouteError } from '../http-util.js';
11
- import { handleApplicationDraftRoute } from './application-drafts.js';
12
11
  import { handleBusinessGrants, handleManagedRecords, handleSolutionCatalog, handleSolutionInstallations, } from './business-information.js';
13
12
  import { handleApplicationActivity, } from './business-information-activity.js';
14
13
  import { handleBusinessChannels, } from './business-information-channels.js';
@@ -22,6 +21,7 @@ import { handleBusinessInformationReaderFloor } from './business-information-rea
22
21
  import { handleBusinessSettings } from './business-information-settings.js';
23
22
  import { handleCollectionSource } from './business-information-source.js';
24
23
  import { handleBusinessInvitationAccept, handleBusinessInvitations, handleEligibleBusinessAssignees, handleMySolutionInstallations, } from './business-information-staff.js';
24
+ import { handleNativeRecordLifecycle } from './business-native-lifecycle.js';
25
25
  export function dispatchBusinessInformationRoutes(req, res, url, deps) {
26
26
  const pageRef = parseBusinessPagePath(url.pathname);
27
27
  const publicPage = /^\/v1\/business-pages\/(sol_[a-f0-9]{32})$/.exec(url.pathname);
@@ -37,10 +37,6 @@ export function dispatchBusinessInformationRoutes(req, res, url, deps) {
37
37
  ? handleBusinessPage(req, res, pageRef, pageDeps)
38
38
  : handlePublicBusinessPage(req, res, publicPage[1], pageDeps));
39
39
  }
40
- if (deps.authoring && parseApplicationDraftPath(url.pathname)) {
41
- const draftDeps = { ...deps, ...deps.authoring, maxBody: deps.maxDraftBody ?? deps.maxBody };
42
- return run(req, res, deps, () => handleApplicationDraftRoute(req, res, url, draftDeps));
43
- }
44
40
  const agreement = /^\/v1\/orgs\/([a-z0-9][a-z0-9-]{0,62})\/agreement$/.exec(url.pathname);
45
41
  if (agreement?.[1])
46
42
  return run(req, res, deps, () => handleOrganizationAgreement(req, res, agreement[1], deps));
@@ -78,6 +74,8 @@ export function dispatchBusinessInformationRoutes(req, res, url, deps) {
78
74
  const installationRef = parseSolutionInstallationPath(url.pathname);
79
75
  if (installationRef === undefined)
80
76
  return false;
77
+ if (installationRef.action === 'record-lifecycle')
78
+ return run(req, res, deps, () => handleNativeRecordLifecycle(req, res, installationRef, deps));
81
79
  if (installationRef.action === 'notice')
82
80
  return run(req, res, deps, () => handleBusinessNotice(req, res, installationRef, deps));
83
81
  if ((installationRef.action === 'activity' || installationRef.action === 'coordination') &&
@@ -131,6 +129,13 @@ function run(req, res, deps, handle) {
131
129
  return true;
132
130
  }
133
131
  function respondBusinessError(deps, res, event, error) {
132
+ if (error instanceof BusinessWorkspaceError) {
133
+ sendJson(res, error.code === 'forbidden' ? 403 : 409, {
134
+ error: 'Business access changed. Refresh before continuing.',
135
+ code: error.code,
136
+ });
137
+ return;
138
+ }
134
139
  if (error instanceof OrganizationAgreementError || error instanceof BusinessNoticeError) {
135
140
  const forbidden = error.code === 'agreement_owner_required' || error.code === 'business_notice_forbidden';
136
141
  sendJson(res, forbidden ? 403 : 409, { error: error.message, code: error.code });
@@ -11,12 +11,14 @@ export function stableInstallationId(org, app, env) {
11
11
  /** Read current activation authority; intake is an independent operator setting. */
12
12
  export async function installationProjection(installation, role, identity, deps) {
13
13
  const state = (await deps.readInstallationActivation?.(installation)) ?? 'unavailable';
14
+ const access = await deps.workspaces?.resolveAccess(installation.scope.org, identity.subject);
14
15
  const canRetry = state === 'pending' &&
15
16
  deps.activateInstallation !== undefined &&
16
17
  role === 'administrator' &&
17
18
  (await canManageMembers(deps.controlPlane, installation.scope.org, identity));
18
19
  return {
19
- ...installationToWire(installation, role),
20
+ ...installationToWire(installation, access?.role ?? role),
21
+ ...(access ? { authorityVersion: 1 } : {}),
20
22
  activation: state === 'pending' ? { state, canRetry } : { state, canRetry: false },
21
23
  };
22
24
  }
@@ -1,16 +1,20 @@
1
1
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
2
2
  import { BusinessNoticeResponseSchema, BusinessNoticeSaveRequestSchema, OrganizationAgreementAcceptRequestSchema, OrganizationAgreementResponseSchema, } from '@noodle-borg/wire-contracts';
3
3
  import { admitBusinessTarget } from '../business-api-admission.js';
4
+ import { businessGrantAllows } from '../business-information/model.js';
4
5
  import { sendForbidden } from '../http-util.js';
5
6
  import { invalid, methodNotAllowed, requireIdentity, requireInstallationGrant, requireInstallationPermission, } from './business-information.js';
6
7
  import { canManageMembers } from './org-admin.js';
7
8
  export async function handleOrganizationAgreement(req, res, org, deps) {
8
9
  if (req.method !== 'GET' && req.method !== 'POST')
9
10
  return methodNotAllowed(res);
11
+ res.setHeader('cache-control', 'private, no-store');
10
12
  const identity = await requireIdentity(req, res, deps);
11
13
  if (identity === false)
12
14
  return;
13
- if (!(await canManageMembers(deps.controlPlane, org, identity)))
15
+ const selected = await deps.workspaces?.authorize(org, identity.subject, 'settings:manage');
16
+ if (selected === 'denied' ||
17
+ (selected !== 'allowed' && !(await canManageMembers(deps.controlPlane, org, identity))))
14
18
  return sendForbidden(res, 'organization owner required');
15
19
  if (!(await admitBusinessTarget(res, { org })))
16
20
  return;
@@ -62,7 +66,7 @@ export async function handleBusinessNotice(req, res, ref, deps) {
62
66
  data: {
63
67
  revision: record?.revision ?? 0,
64
68
  notice: record?.notice ?? null,
65
- canEdit: authorized.grant.role === 'administrator',
69
+ canEdit: businessGrantAllows(authorized.grant, 'installation:administer'),
66
70
  },
67
71
  }));
68
72
  }
@@ -59,7 +59,7 @@ export async function handleBusinessPage(req, res, ref, deps) {
59
59
  if (!authorized)
60
60
  return;
61
61
  try {
62
- let record = await deps.store.pages.get(authorized.scope);
62
+ let record = await deps.store.staff.run(authorized.scope, identity.subject, 'installation:administer', () => deps.store.pages.get(authorized.scope), () => new BusinessPageError('business_page_forbidden'));
63
63
  if (req.method !== 'GET') {
64
64
  const body = await readJsonBody(req, Math.min(deps.maxBody, 256 * 1024));
65
65
  if (!body.ok)
@@ -27,7 +27,7 @@ export function parseSolutionInstallationPath(pathname) {
27
27
  ...(connectionAction === undefined ? {} : { connectionAction }),
28
28
  };
29
29
  }
30
- const application = /^\/v1\/orgs\/([^/]+)\/solution-installations\/([^/]+)\/(activate|settings|notice|channels|operations\/coordination(?:\/resolve)?|activity(?:\/(?:settings|export|preview))?)$/.exec(pathname);
30
+ const application = /^\/v1\/orgs\/([^/]+)\/solution-installations\/([^/]+)\/(activate|settings|notice|record-lifecycle|channels|operations\/coordination(?:\/resolve)?|activity(?:\/(?:settings|export|preview))?)$/.exec(pathname);
31
31
  if (application !== null) {
32
32
  const org = decoded(application, 1);
33
33
  const installationId = decoded(application, 2);
@@ -1,5 +1,6 @@
1
1
  import { sendJson } from '@noodle-borg/transport-http';
2
2
  import { PayloadValidationError, } from '../business-information/portable.js';
3
+ import { runBusinessStaffOperation } from './business-information-access.js';
3
4
  export async function createNativeRecord(res, deps, input) {
4
5
  try {
5
6
  const request = {
@@ -11,13 +12,17 @@ export async function createNativeRecord(res, deps, input) {
11
12
  actorSubject: input.actorSubject,
12
13
  };
13
14
  if (input.admit) {
14
- const receipt = await deps.store.probeRequest(request);
15
+ const receipt = input.origin.kind === 'portal'
16
+ ? await runBusinessStaffOperation(deps, request.scope, input.actorSubject, 'records:create', () => deps.store.probeRequest(request))
17
+ : await deps.store.probeRequest(request);
15
18
  if (receipt.disposition !== 'missing')
16
19
  return receipt;
17
20
  if (!(await input.admit()))
18
21
  return undefined;
19
22
  }
20
- return await deps.store.createRequest(request);
23
+ return input.origin.kind === 'portal'
24
+ ? await runBusinessStaffOperation(deps, request.scope, input.actorSubject, 'records:create', () => deps.store.createRequest(request))
25
+ : await deps.store.createRequest(request);
21
26
  }
22
27
  catch (error) {
23
28
  if (!(error instanceof PayloadValidationError))