@noodleseed/one 0.174.0 → 0.175.1

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 (164) 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.d.ts.map +1 -1
  11. package/dist/commands/solutions-ops.js +3 -0
  12. package/dist/commands/solutions-ops.js.map +1 -1
  13. package/dist/commands/solutions-workspace.d.ts.map +1 -1
  14. package/dist/commands/solutions-workspace.js +36 -11
  15. package/dist/commands/solutions-workspace.js.map +1 -1
  16. package/dist/commands/whatsapp-output.d.ts.map +1 -1
  17. package/dist/commands/whatsapp-output.js +20 -4
  18. package/dist/commands/whatsapp-output.js.map +1 -1
  19. package/node_modules/@noodle-borg/admission-limits/dist/public-record-admission.d.ts +16 -9
  20. package/node_modules/@noodle-borg/admission-limits/dist/public-record-admission.js +5 -2
  21. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +9 -0
  22. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +5 -5
  23. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +9 -0
  24. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +1 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +13 -1
  26. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  27. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +5 -3
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +5 -4
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +6 -2
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +1 -1
  31. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +12 -0
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +7 -2
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +14 -4
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-capture.js +104 -0
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-judgment.js +93 -0
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-ledger.d.ts +105 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-ledger.js +183 -0
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-model-call.js +38 -0
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-parsers.d.ts +22 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-parsers.js +109 -0
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-review.d.ts +21 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-review.js +44 -0
  43. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-spec.d.ts +18 -0
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-spec.js +86 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-store.d.ts +11 -0
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/collection-store.js +18 -0
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +20 -1
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +38 -2
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/model-runtime.js +3 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +5 -0
  51. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +5 -0
  52. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  53. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
  54. package/node_modules/@noodle-borg/authoring/dist/interaction.d.ts +49 -0
  55. package/node_modules/@noodle-borg/authoring/dist/interaction.js +25 -0
  56. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
  57. package/node_modules/@noodle-borg/authoring/dist/server.js +4 -0
  58. package/node_modules/@noodle-borg/authoring/package.json +6 -1
  59. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +1 -1
  60. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-drafts.js +7 -0
  61. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-native-lifecycle.js +43 -0
  62. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-records.js +154 -0
  63. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-workspace.js +8 -1
  64. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +2 -149
  65. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +2 -1
  66. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +33 -0
  67. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
  68. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
  69. package/node_modules/@noodle-borg/compiler/dist/compile.js +5 -0
  70. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  71. package/node_modules/@noodle-borg/compiler/dist/index.d.ts +2 -2
  72. package/node_modules/@noodle-borg/compiler/dist/index.js +1 -1
  73. package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-schema.d.ts +41 -0
  74. package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-schema.js +62 -0
  75. package/node_modules/@noodle-borg/compiler/dist/manifest/interaction-validation.js +220 -0
  76. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +60 -4
  77. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +7 -1
  78. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +9 -1
  79. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +58 -10
  80. package/node_modules/@noodle-borg/control-plane/dist/in-memory-atomic-state.d.ts +8 -0
  81. package/node_modules/@noodle-borg/control-plane/dist/in-memory-atomic-state.js +136 -0
  82. package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.d.ts +7 -0
  83. package/node_modules/@noodle-borg/control-plane/dist/in-memory-control-plane-store.js +16 -24
  84. package/node_modules/@noodle-borg/control-plane/dist/in-memory-organization-store.d.ts +4 -2
  85. package/node_modules/@noodle-borg/control-plane/dist/in-memory-organization-store.js +8 -6
  86. package/node_modules/@noodle-borg/control-plane/dist/mcp-subdomain-claims.d.ts +2 -1
  87. package/node_modules/@noodle-borg/control-plane/dist/mcp-subdomain-claims.js +13 -8
  88. package/node_modules/@noodle-borg/control-plane/dist/organization-agreements.d.ts +13 -4
  89. package/node_modules/@noodle-borg/control-plane/dist/organization-agreements.js +23 -7
  90. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +2 -1
  91. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  92. package/node_modules/@noodle-borg/module/dist/platform-human-identity.d.ts +5 -2
  93. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +16 -4
  94. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +3 -6
  95. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
  96. package/node_modules/@noodle-borg/service/dist/application-activity.js +85 -63
  97. package/node_modules/@noodle-borg/service/dist/application-drafts/contracts.js +9 -0
  98. package/node_modules/@noodle-borg/service/dist/application-drafts/paths.js +2 -2
  99. package/node_modules/@noodle-borg/service/dist/application-runtime-target.js +1 -1
  100. package/node_modules/@noodle-borg/service/dist/business-information/contracts.d.ts +18 -4
  101. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-business-pages.js +3 -5
  102. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-installation-lifecycle.js +8 -6
  103. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-native-lifecycle.js +80 -0
  104. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +27 -31
  105. package/node_modules/@noodle-borg/service/dist/business-information/memory-native-retention.js +29 -0
  106. package/node_modules/@noodle-borg/service/dist/business-information/model.js +9 -2
  107. package/node_modules/@noodle-borg/service/dist/business-information/native-query.js +3 -1
  108. package/node_modules/@noodle-borg/service/dist/business-information/native-record-lifecycle.d.ts +27 -0
  109. package/node_modules/@noodle-borg/service/dist/business-information/native-record-lifecycle.js +63 -0
  110. package/node_modules/@noodle-borg/service/dist/business-information/principal-authority.d.ts +3 -1
  111. package/node_modules/@noodle-borg/service/dist/business-information/principal-authority.js +9 -4
  112. package/node_modules/@noodle-borg/service/dist/business-information/staff-authority.d.ts +16 -0
  113. package/node_modules/@noodle-borg/service/dist/business-information/staff-authority.js +60 -0
  114. package/node_modules/@noodle-borg/service/dist/business-information/workspace-permissions.js +18 -0
  115. package/node_modules/@noodle-borg/service/dist/business-information-runtime.js +1 -0
  116. package/node_modules/@noodle-borg/service/dist/business-onboarding.js +16 -3
  117. package/node_modules/@noodle-borg/service/dist/business-workspaces/contracts.d.ts +110 -0
  118. package/node_modules/@noodle-borg/service/dist/business-workspaces/permissions.d.ts +7 -0
  119. package/node_modules/@noodle-borg/service/dist/business-workspaces/store.d.ts +102 -0
  120. package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +70 -8
  121. package/node_modules/@noodle-borg/service/dist/channels/capability-report.js +73 -0
  122. package/node_modules/@noodle-borg/service/dist/channels/collection-turn.js +232 -0
  123. package/node_modules/@noodle-borg/service/dist/channels/composition.js +1 -0
  124. package/node_modules/@noodle-borg/service/dist/channels/messaging-eligibility.js +45 -0
  125. package/node_modules/@noodle-borg/service/dist/channels/reply-buttons.js +23 -0
  126. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +127 -33
  127. package/node_modules/@noodle-borg/service/dist/native-record-connector.js +19 -4
  128. package/node_modules/@noodle-borg/service/dist/routes/application-drafts.js +44 -2
  129. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +4 -0
  130. package/node_modules/@noodle-borg/service/dist/routes/business-authoring-dispatch.js +1 -1
  131. package/node_modules/@noodle-borg/service/dist/routes/business-information-access.js +39 -0
  132. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +1 -1
  133. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +2 -2
  134. package/node_modules/@noodle-borg/service/dist/routes/business-information-connections.js +10 -4
  135. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +11 -0
  136. package/node_modules/@noodle-borg/service/dist/routes/business-information-installation.js +3 -1
  137. package/node_modules/@noodle-borg/service/dist/routes/business-information-notice.js +6 -2
  138. package/node_modules/@noodle-borg/service/dist/routes/business-information-pages.js +1 -1
  139. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +1 -1
  140. package/node_modules/@noodle-borg/service/dist/routes/business-information-record-create.js +7 -2
  141. package/node_modules/@noodle-borg/service/dist/routes/business-information-record-reads.js +2 -2
  142. package/node_modules/@noodle-borg/service/dist/routes/business-information-records.js +332 -0
  143. package/node_modules/@noodle-borg/service/dist/routes/business-information-settings.js +6 -3
  144. package/node_modules/@noodle-borg/service/dist/routes/business-information-staff.js +9 -5
  145. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +12 -356
  146. package/node_modules/@noodle-borg/service/dist/routes/business-native-lifecycle.js +46 -0
  147. package/node_modules/@noodle-borg/service/dist/routes/business-workspaces.js +14 -1
  148. package/node_modules/@noodle-borg/service/dist/service.js +1 -0
  149. package/node_modules/@noodle-borg/service/dist/web-capabilities.js +3 -1
  150. package/node_modules/@noodle-borg/service/package.json +2 -1
  151. package/node_modules/@noodle-borg/wire-contracts/dist/application-draft-validation.d.ts +59 -0
  152. package/node_modules/@noodle-borg/wire-contracts/dist/application-draft-validation.js +38 -0
  153. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +144 -50
  154. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +44 -4
  155. package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.d.ts +70 -0
  156. package/node_modules/@noodle-borg/wire-contracts/dist/business-workspaces.js +31 -1
  157. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +2 -0
  158. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +2 -0
  159. package/node_modules/@noodle-borg/wire-contracts/dist/native-record-lifecycle.d.ts +81 -0
  160. package/node_modules/@noodle-borg/wire-contracts/dist/native-record-lifecycle.js +39 -0
  161. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +26 -0
  162. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +24 -1
  163. package/node_modules/@noodleseed/assistant/package.json +1 -1
  164. package/package.json +2 -2
@@ -4,6 +4,7 @@ import { ApplicationActivityListResponseSchema, ApplicationActivityPreviewRespon
4
4
  import { resolveApplicationRuntimeTarget } from './application-runtime-target.js';
5
5
  import { createOperationCoordinationPort, } from './operation-coordination.js';
6
6
  import { createOperationEvidencePort, } from './operation-evidence.js';
7
+ const runLocal = (operation) => operation();
7
8
  const POLICY_MESSAGES = {
8
9
  activity_conflict: 'History settings or plan changed. Reload before continuing.',
9
10
  activity_invalid: 'Activity paging or retention is invalid for the current policy.',
@@ -25,8 +26,8 @@ export class ApplicationActivity {
25
26
  constructor(options) {
26
27
  this.options = options;
27
28
  }
28
- /** Shared strict operator projection. Callers authorize the live installation grant before and after it. */
29
- async project(scope, action, input) {
29
+ /** Callers authorize before/after projection and fence each local effect with current authority. */
30
+ async project(scope, action, input, local = runLocal) {
30
31
  const coordinating = action.startsWith('coordination');
31
32
  const invalid = () => new ActivityPolicyError(coordinating ? 'coordination_invalid' : 'activity_invalid');
32
33
  const allowed = action === 'coordination-list'
@@ -51,16 +52,17 @@ export class ApplicationActivity {
51
52
  const store = this.options.coordination;
52
53
  if (!store)
53
54
  throw new ActivityPolicyError('coordination_unavailable');
54
- let resolved;
55
- try {
56
- resolved = await store.resolve(scope, parsed.data.resource, parsed.data.token, {
57
- reviewer: input.reviewer,
58
- reason: parsed.data.reason,
59
- });
60
- }
61
- catch {
62
- throw new ActivityPolicyError('coordination_unavailable');
63
- }
55
+ const resolved = await local(async () => {
56
+ try {
57
+ return await store.resolve(scope, parsed.data.resource, parsed.data.token, {
58
+ reviewer: input.reviewer,
59
+ reason: parsed.data.reason,
60
+ });
61
+ }
62
+ catch {
63
+ throw new ActivityPolicyError('coordination_unavailable');
64
+ }
65
+ });
64
66
  if (!resolved)
65
67
  throw new ActivityPolicyError('coordination_conflict');
66
68
  return mutation(OperationCoordinationResolveResponseSchema.parse({ ok: true, data: { resolved: true } }), 'config.operation.coordination_resolved', { resource: parsed.data.resource });
@@ -77,52 +79,56 @@ export class ApplicationActivity {
77
79
  const store = this.options.coordination;
78
80
  if (!store)
79
81
  throw new ActivityPolicyError('coordination_unavailable');
80
- try {
81
- const limit = parsed.data.limit ?? 100;
82
- const records = await store.list(scope, limit, parsed.data.beforeResource);
83
- if (records.some((record) => canonicalJson(record.scope) !== canonicalJson(scope)))
84
- throw new Error('Coordination scope mismatch');
85
- return {
86
- response: OperationCoordinationListResponseSchema.parse({
87
- ok: true,
88
- data: {
89
- records: records.map(({ resource, token, reference, operationDigest, startedAt, deadline, state }) => ({
90
- resource,
91
- token,
92
- reference,
93
- operationDigest,
94
- startedAt: new Date(startedAt).toISOString(),
95
- deadline: new Date(deadline).toISOString(),
96
- state,
97
- })),
98
- ...(records.length === limit ? { nextBeforeResource: records.at(-1)?.resource } : {}),
99
- },
100
- }),
101
- };
102
- }
103
- catch {
104
- throw new ActivityPolicyError('coordination_unavailable');
105
- }
82
+ return local(async () => {
83
+ try {
84
+ const limit = parsed.data.limit ?? 100;
85
+ const records = await store.list(scope, limit, parsed.data.beforeResource);
86
+ if (records.some((record) => canonicalJson(record.scope) !== canonicalJson(scope)))
87
+ throw new Error('Coordination scope mismatch');
88
+ return {
89
+ response: OperationCoordinationListResponseSchema.parse({
90
+ ok: true,
91
+ data: {
92
+ records: records.map(({ resource, token, reference, operationDigest, startedAt, deadline, state, }) => ({
93
+ resource,
94
+ token,
95
+ reference,
96
+ operationDigest,
97
+ startedAt: new Date(startedAt).toISOString(),
98
+ deadline: new Date(deadline).toISOString(),
99
+ state,
100
+ })),
101
+ ...(records.length === limit
102
+ ? { nextBeforeResource: records.at(-1)?.resource }
103
+ : {}),
104
+ },
105
+ }),
106
+ };
107
+ }
108
+ catch {
109
+ throw new ActivityPolicyError('coordination_unavailable');
110
+ }
111
+ });
106
112
  }
107
113
  if (action === 'save-settings') {
108
114
  const parsed = ApplicationActivitySettingsSaveRequestSchema.safeParse(input.body);
109
115
  if (!parsed.success)
110
116
  throw invalid();
111
- const data = await this.save(scope, parsed.data);
117
+ const data = await this.save(scope, parsed.data, local);
112
118
  return mutation(ApplicationActivitySettingsResponseSchema.parse({ ok: true, data }), 'config.activity.retention_changed', { retentionDays: data.retentionDays });
113
119
  }
114
120
  if (action === 'preview')
115
121
  return {
116
122
  response: ApplicationActivityPreviewResponseSchema.parse({
117
123
  ok: true,
118
- data: await this.preview(scope),
124
+ data: await this.preview(scope, local),
119
125
  }),
120
126
  };
121
127
  if (action === 'settings')
122
128
  return {
123
129
  response: ApplicationActivitySettingsResponseSchema.parse({
124
130
  ok: true,
125
- data: (await this.settings(scope, input.canEdit)).projection,
131
+ data: (await this.settings(scope, input.canEdit, local)).projection,
126
132
  }),
127
133
  };
128
134
  const limit = parameters.has('limit') ? Number(parameters.get('limit')) : 50;
@@ -139,11 +145,11 @@ export class ApplicationActivity {
139
145
  purpose: action,
140
146
  limit,
141
147
  ...(cursor === undefined ? {} : { cursor }),
142
- }),
148
+ }, local),
143
149
  }),
144
150
  };
145
151
  }
146
- async settings(scope, canEdit) {
152
+ async settings(scope, canEdit, local = runLocal) {
147
153
  const allowance = await this.options.allowance(scope.org);
148
154
  if (!allowance ||
149
155
  !validDays(allowance.maximumDays) ||
@@ -151,11 +157,14 @@ export class ApplicationActivity {
151
157
  allowance.defaultDays > allowance.maximumDays ||
152
158
  !allowance.revision)
153
159
  throw new ActivityPolicyError('activity_unavailable');
154
- let setting = await this.options.store.readRetention(scope);
155
- if (!setting) {
156
- await this.options.store.setRetention(scope, allowance.defaultDays, undefined);
157
- setting = await this.options.store.readRetention(scope);
158
- }
160
+ const setting = await local(async () => {
161
+ let value = await this.options.store.readRetention(scope);
162
+ if (!value) {
163
+ await this.options.store.setRetention(scope, allowance.defaultDays, undefined);
164
+ value = await this.options.store.readRetention(scope);
165
+ }
166
+ return value;
167
+ });
159
168
  if (!setting || !validDays(setting.days))
160
169
  throw new ActivityPolicyError('activity_unavailable');
161
170
  return {
@@ -169,28 +178,28 @@ export class ApplicationActivity {
169
178
  },
170
179
  };
171
180
  }
172
- async save(scope, input) {
173
- const current = await this.settings(scope, true);
181
+ async save(scope, input, local = runLocal) {
182
+ const current = await this.settings(scope, true, local);
174
183
  if (current.projection.revision !== input.expectedRevision)
175
184
  throw new ActivityPolicyError('activity_conflict');
176
185
  if (!validDays(input.retentionDays) || input.retentionDays > current.allowance.maximumDays)
177
186
  throw new ActivityPolicyError('activity_invalid');
178
- if (!(await this.options.store.setRetention(scope, input.retentionDays, current.setting.revision)))
187
+ if (!(await local(() => this.options.store.setRetention(scope, input.retentionDays, current.setting.revision))))
179
188
  throw new ActivityPolicyError('activity_conflict');
180
- return (await this.settings(scope, true)).projection;
189
+ return (await this.settings(scope, true, local)).projection;
181
190
  }
182
- async page(scope, input) {
191
+ async page(scope, input, local = runLocal) {
183
192
  if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100)
184
193
  throw new ActivityPolicyError('activity_invalid');
185
- const policy = (await this.settings(scope, false)).projection;
194
+ const policy = (await this.settings(scope, false, local)).projection;
186
195
  const binding = this.hash({
187
196
  purpose: `activity-${input.purpose}-v1`,
188
197
  scope,
189
198
  revision: policy.revision,
190
199
  limit: input.limit,
191
200
  });
192
- const records = await this.options.store.list(scope, this.options.now?.() ?? Date.now(), policy.maximumDays, input.limit + 1, decodeActivityCursor(input.cursor, binding));
193
- if ((await this.settings(scope, false)).projection.revision !== policy.revision)
201
+ const records = await local(() => this.options.store.list(scope, this.options.now?.() ?? Date.now(), policy.retentionDays, input.limit + 1, decodeActivityCursor(input.cursor, binding)));
202
+ if ((await this.settings(scope, false, local)).projection.revision !== policy.revision)
194
203
  throw new ActivityPolicyError('activity_conflict');
195
204
  const page = records.slice(0, input.limit);
196
205
  const last = page.at(-1);
@@ -208,7 +217,7 @@ export class ApplicationActivity {
208
217
  : { completedAt: new Date(completedAt).toISOString() }),
209
218
  ...(reference === undefined ? {} : { reference }),
210
219
  })),
211
- historyDays: policy.maximumDays,
220
+ historyDays: policy.retentionDays,
212
221
  ...(records.length > page.length && last
213
222
  ? {
214
223
  nextCursor: Buffer.from(JSON.stringify({ binding, startedAt: last.startedAt, id: last.id })).toString('base64url'),
@@ -224,7 +233,7 @@ export class ApplicationActivity {
224
233
  revision(scope, setting, allowance) {
225
234
  return sha256Canonical({ scope, setting, allowance });
226
235
  }
227
- async preview(scope) {
236
+ async preview(scope, local = runLocal) {
228
237
  const unavailable = () => {
229
238
  throw new ActivityPolicyError('activity_unavailable');
230
239
  };
@@ -254,16 +263,28 @@ export class ApplicationActivity {
254
263
  !validDays(maximumDays) ||
255
264
  maximumDays >= allowance.maximumDays))
256
265
  throw new ActivityPolicyError('activity_unavailable');
257
- const counts = await this.options.store
266
+ // A comparison is read-only: use the verified default without initializing a setting.
267
+ const readSetting = () => local(() => this.options.store.readRetention(scope).catch(unavailable));
268
+ const setting = await readSetting();
269
+ if (setting && !validDays(setting.days))
270
+ throw new ActivityPolicyError('activity_unavailable');
271
+ const selectedDays = setting?.days ?? allowance.defaultDays;
272
+ const counts = await local(() => this.options.store
258
273
  .preview(scope, {
259
274
  asOf,
260
275
  paidPeriodEnd,
261
- currentMaximumDays: allowance.maximumDays,
262
- scenarios: preview.scenarios,
276
+ currentMaximumDays: Math.min(selectedDays, allowance.maximumDays),
277
+ scenarios: preview.scenarios.map(({ id, maximumDays }) => ({
278
+ id,
279
+ maximumDays: Math.min(selectedDays, maximumDays),
280
+ })),
263
281
  })
264
- .catch(unavailable);
282
+ .catch(unavailable));
265
283
  const current = await resolve();
266
- if (current?.revision !== allowance.revision)
284
+ const currentSetting = await readSetting();
285
+ if (current?.revision !== allowance.revision ||
286
+ currentSetting?.revision !== setting?.revision ||
287
+ currentSetting?.days !== setting?.days)
267
288
  throw new ActivityPolicyError('activity_conflict');
268
289
  return {
269
290
  state: 'available',
@@ -274,6 +295,7 @@ export class ApplicationActivity {
274
295
  purpose: 'activity-preview-v1',
275
296
  scope,
276
297
  allowance: allowance.revision,
298
+ setting: setting ?? null,
277
299
  preview,
278
300
  counts,
279
301
  }),
@@ -8,4 +8,13 @@ export class ApplicationDraftError extends Error {
8
8
  this.name = 'ApplicationDraftError';
9
9
  }
10
10
  }
11
+ /** Service-facing availability only; source errors remain bounded validation reports. */
12
+ export class DraftValidationUnavailableError extends Error {
13
+ code;
14
+ constructor(code) {
15
+ super(code);
16
+ this.code = code;
17
+ this.name = 'DraftValidationUnavailableError';
18
+ }
19
+ }
11
20
  //# sourceMappingURL=contracts.js.map
@@ -1,7 +1,7 @@
1
1
  import { validateSlug } from '@noodle-borg/control-plane/portable';
2
2
  import { ApplicationDraftIdSchema } from '@noodle-borg/wire-contracts';
3
3
  export function parseApplicationDraftPath(path) {
4
- const match = /^\/v1\/orgs\/([a-z0-9-]+)\/apps\/([a-z0-9-]+)\/drafts(?:\/([a-f0-9-]+)(?:\/(undo|history|diff))?)?$/.exec(path);
4
+ const match = /^\/v1\/orgs\/([a-z0-9-]+)\/apps\/([a-z0-9-]+)\/drafts(?:\/([a-f0-9-]+)(?:\/(undo|history|diff|validate))?)?$/.exec(path);
5
5
  const org = match?.[1], app = match?.[2], id = match?.[3], action = match?.[4];
6
6
  if (!org || !app || (id !== undefined && !ApplicationDraftIdSchema.safeParse(id).success))
7
7
  return undefined;
@@ -15,7 +15,7 @@ export function parseApplicationDraftPath(path) {
15
15
  return { org, app, ...(id ? { id } : {}), ...(action ? { action } : {}) };
16
16
  }
17
17
  export function applicationDraftMethods(ref) {
18
- return ref.action === 'undo'
18
+ return ref.action === 'undo' || ref.action === 'validate'
19
19
  ? ['POST']
20
20
  : ref.action
21
21
  ? ['GET']
@@ -120,7 +120,7 @@ export function createApplicationServingRuntime(registry, providerOptions, contr
120
120
  const options = providerOptions;
121
121
  const businessInformationStore = options.businessInformationStore;
122
122
  const businessOnboarding = options.businessOnboarding !== undefined && businessInformationStore
123
- ? new BusinessOnboarding(options.businessOnboarding, controlPlane, businessInformationStore)
123
+ ? new BusinessOnboarding(options.businessOnboarding, controlPlane, businessInformationStore, options.businessAuthoring?.workspaces)
124
124
  : undefined;
125
125
  const activateInstallation = (input) => activateSolutionInstallation(input, {
126
126
  registry,
@@ -1,8 +1,11 @@
1
1
  import type { ArtifactVariableDeclaration, ManagedCollectionControls } from '@noodle-borg/compiler';
2
- import type { ManagedRecordQuery } from '@noodle-borg/wire-contracts';
2
+ import type { EligibleBusinessAssignee, ManagedRecordQuery } from '@noodle-borg/wire-contracts';
3
+ import type { BusinessWorkspaceAccess } from '../business-workspaces/contracts.js';
3
4
  import type { BusinessNoticeStore } from './business-notice.js';
4
5
  import type { BusinessPageStore } from './business-page.js';
6
+ import type { NativeRecordLifecycleStore } from './native-record-lifecycle.js';
5
7
  import type { BusinessPrincipalProvider } from './principal-authority.js';
8
+ import type { BusinessStaffAuthority } from './staff-authority.js';
6
9
  export type JsonPrimitive = string | number | boolean | null;
7
10
  export type JsonValue = JsonPrimitive | JsonObject | readonly JsonValue[];
8
11
  export interface JsonObject {
@@ -86,6 +89,8 @@ export interface SolutionInstallation {
86
89
  readonly managedCollections: readonly string[];
87
90
  readonly definition: SolutionDefinitionSnapshot;
88
91
  readonly retentionDays: ManagedRetentionDays;
92
+ /** Absent on legacy installations until reviewed lifecycle migration. */
93
+ readonly nativeRecordLifecycle?: 'explicit_erasure';
89
94
  /** Public native-record intake exposure; authorized records remain operable while paused. */
90
95
  readonly intakeActive: boolean;
91
96
  /** Internal binding to the authoritative app incarnation; never a Portal configuration value. */
@@ -97,6 +102,7 @@ export interface SolutionInstallation {
97
102
  readonly updatedBySubject: string;
98
103
  }
99
104
  export interface BusinessGrant {
105
+ readonly authorityVersion?: never;
100
106
  readonly scope: InstallationScope;
101
107
  readonly subject: string;
102
108
  readonly email?: string;
@@ -108,6 +114,11 @@ export interface BusinessGrant {
108
114
  readonly updatedBySubject: string;
109
115
  readonly revokedAt?: string;
110
116
  }
117
+ /** Request-scoped access projection. Versioned access is never persisted as an installation grant. */
118
+ export type BusinessStaffGrant = BusinessGrant | (BusinessWorkspaceAccess & {
119
+ readonly scope: InstallationScope;
120
+ readonly revokedAt?: never;
121
+ });
111
122
  export interface BusinessInvitation {
112
123
  readonly scope: InstallationScope;
113
124
  readonly invitationId: string;
@@ -127,7 +138,8 @@ export interface BusinessInvitation {
127
138
  readonly revokedBySubject?: string;
128
139
  }
129
140
  export interface ManagedRequestOrigin {
130
- readonly kind: 'embedded' | 'mcp' | 'portal' | 'api' | 'import';
141
+ /** `messaging` is trusted channel provenance (ADR 0240); its reference names the channel, e.g. `whatsapp`. */
142
+ readonly kind: 'embedded' | 'mcp' | 'portal' | 'api' | 'import' | 'messaging';
131
143
  readonly reference?: string;
132
144
  }
133
145
  export interface ManagedRequestNote {
@@ -157,7 +169,7 @@ export interface ManagedRequestRecord {
157
169
  readonly assigneeSubject?: string;
158
170
  readonly origin: ManagedRequestOrigin;
159
171
  readonly revision: number;
160
- readonly retentionExpiresAt: string;
172
+ readonly retentionExpiresAt: string | null;
161
173
  readonly createdAt: string;
162
174
  readonly createdBySubject: string;
163
175
  readonly updatedAt: string;
@@ -435,7 +447,9 @@ export interface ManagedRequestStore {
435
447
  }
436
448
  export interface BusinessInformationStore extends SolutionInstallationStore, BusinessNoticeStore, BusinessGrantStore, ManagedRequestStore {
437
449
  readonly pages: BusinessPageStore;
450
+ readonly nativeLifecycle: NativeRecordLifecycleStore;
451
+ readonly staff: BusinessStaffAuthority;
438
452
  configurePrincipalAuthority(provider: BusinessPrincipalProvider | undefined): void;
439
- listEligibleAssignees(scope: InstallationScope): Promise<readonly BusinessGrant[]>;
453
+ listEligibleAssignees(scope: InstallationScope, actor: string): Promise<readonly EligibleBusinessAssignee[]>;
440
454
  }
441
455
  //# sourceMappingURL=contracts.d.ts.map
@@ -21,17 +21,15 @@ export class InMemoryBusinessPages {
21
21
  const scope = validateScope(input.scope);
22
22
  const actorSubject = validateScalar('page actor', input.actorSubject, 500);
23
23
  const key = scopeKey(scope);
24
- return this.locks.run(`grants:${key}`, () => this.locks.run(`intake:${key}`, async () => {
25
- const grant = await this.business.getGrant(scope, actorSubject);
24
+ return this.business.staff.run(scope, actorSubject, 'installation:administer', () => this.locks.run(`grants:${key}`, () => this.locks.run(`intake:${key}`, async () => {
26
25
  if (!(await this.business.getInstallation(scope)) ||
27
- grant?.role !== 'administrator' ||
28
- grant.revokedAt ||
26
+ !(await this.business.staff.allows(scope, actorSubject, 'installation:administer')) ||
29
27
  !(await this.principals.allows(actorSubject)))
30
28
  throw new BusinessPageError('business_page_forbidden');
31
29
  const next = await nextBusinessPage(this.#records.get(key), { ...input, scope, actorSubject }, this.now().toISOString(), await this.business.getBusinessNotice(scope), assertReady);
32
30
  this.#records.set(key, structuredClone(next));
33
31
  return next;
34
- }));
32
+ })), () => new BusinessPageError('business_page_forbidden'));
35
33
  }
36
34
  }
37
35
  //# sourceMappingURL=in-memory-business-pages.js.map
@@ -10,10 +10,12 @@ export class InMemoryInstallationLifecycle {
10
10
  #managedDefinition;
11
11
  #application;
12
12
  #notices = new Map();
13
- #getGrant;
13
+ #staff;
14
+ #principals;
14
15
  constructor(input) {
15
16
  this.#installations = input.installations;
16
- this.#getGrant = input.getGrant;
17
+ this.#staff = input.staff;
18
+ this.#principals = input.principals;
17
19
  this.#withLock = input.withLock;
18
20
  this.#now = input.now;
19
21
  this.#managedDefinition = input.managedDefinition;
@@ -35,9 +37,9 @@ export class InMemoryInstallationLifecycle {
35
37
  async setBusinessNotice(input) {
36
38
  const scope = validateScope(input.scope);
37
39
  const notice = validatedNotice(input);
38
- return this.#withLock(`grants:${scopeKey(scope)}`, async () => {
39
- const grant = this.#getGrant(scope, input.actorSubject);
40
- if (grant?.role !== 'administrator' || grant.revokedAt)
40
+ return this.#staff.run(scope, input.actorSubject, 'installation:administer', () => this.#withLock(`grants:${scopeKey(scope)}`, async () => {
41
+ if (!(await this.#staff.allows(scope, input.actorSubject, 'installation:administer')) ||
42
+ !(await this.#principals.allows(input.actorSubject)))
41
43
  throw new BusinessNoticeError('business_notice_forbidden');
42
44
  const key = scopeKey(scope);
43
45
  const current = this.#notices.get(key);
@@ -51,7 +53,7 @@ export class InMemoryInstallationLifecycle {
51
53
  };
52
54
  this.#notices.set(key, record);
53
55
  return structuredClone(record);
54
- });
56
+ }), () => new BusinessNoticeError('business_notice_forbidden'));
55
57
  }
56
58
  configureApplicationLifecycle(resolve) {
57
59
  this.#application = resolve;
@@ -0,0 +1,80 @@
1
+ import { checkLifecycleReview, lifecyclePreview, NativeLifecycleError, sameLifecycleReview, } from './native-record-lifecycle.js';
2
+ import { nativeCustodyBytes } from './native-storage-budget.js';
3
+ import { scopeKey } from './pagination.js';
4
+ import { validateScalar, validateScope } from './validation.js';
5
+ /** Local/test only. No await between inventory verification and the synchronous atomic commit. */
6
+ export class InMemoryNativeRecordLifecycle {
7
+ options;
8
+ #receipts = new Map();
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ preview(scope, actor) {
13
+ return this.run(scope, actor, (installation) => lifecyclePreview(installation, this.inventory(scope), this.options.now().toISOString()));
14
+ }
15
+ migrate(input) {
16
+ return this.run(input.scope, input.actor, (installation) => {
17
+ const key = scopeKey(input.scope);
18
+ const receipt = this.#receipts.get(key);
19
+ if (receipt &&
20
+ sameLifecycleReview(receipt.preview, input.preview) &&
21
+ receipt.actor === input.actor)
22
+ return { ...receipt.result, replayed: true };
23
+ const now = this.options.now().toISOString();
24
+ checkLifecycleReview(installation, this.inventory(input.scope), input.preview, now);
25
+ const updates = [];
26
+ let beforeBytes = 0, afterBytes = 0;
27
+ for (const [recordKey, record] of this.options.records) {
28
+ if (scopeKey(record.scope) !== key ||
29
+ record.deletedAt ||
30
+ record.retentionExpiresAt === null ||
31
+ record.retentionExpiresAt <= now)
32
+ continue;
33
+ const next = { ...record, retentionExpiresAt: null };
34
+ beforeBytes += nativeCustodyBytes(record);
35
+ afterBytes += nativeCustodyBytes(next);
36
+ updates.push([recordKey, next]);
37
+ }
38
+ const result = {
39
+ policy: 'explicit_erasure',
40
+ installationRevision: installation.revision + 1,
41
+ recordsPreserved: updates.length,
42
+ replayed: false,
43
+ };
44
+ this.options.budget.replace(key, beforeBytes, afterBytes);
45
+ for (const [id, record] of updates)
46
+ this.options.records.set(id, record);
47
+ this.options.installations.set(key, {
48
+ ...installation,
49
+ nativeRecordLifecycle: 'explicit_erasure',
50
+ revision: result.installationRevision,
51
+ updatedAt: now,
52
+ updatedBySubject: input.actor,
53
+ });
54
+ this.#receipts.set(key, {
55
+ preview: structuredClone(input.preview),
56
+ actor: input.actor,
57
+ result,
58
+ });
59
+ return result;
60
+ });
61
+ }
62
+ inventory(scope) {
63
+ return [...this.options.records.values()].filter((r) => scopeKey(r.scope) === scopeKey(scope) && !r.deletedAt);
64
+ }
65
+ run(input, actor, work) {
66
+ const scope = validateScope(input);
67
+ validateScalar('actor', actor, 256);
68
+ const { staff, locks, principals } = this.options;
69
+ return staff.run(scope, actor, 'installation:administer', () => locks.run(`grants:${scopeKey(scope)}`, () => locks.run(`intake:${scopeKey(scope)}`, async () => {
70
+ if (!(await staff.allows(scope, actor, 'installation:administer')) ||
71
+ !(await principals.allows(actor)))
72
+ throw new NativeLifecycleError('lifecycle_denied');
73
+ const installation = this.options.installations.get(scopeKey(scope));
74
+ if (!installation)
75
+ throw new NativeLifecycleError('lifecycle_unavailable');
76
+ return work(installation);
77
+ })), () => new NativeLifecycleError('lifecycle_denied'));
78
+ }
79
+ }
80
+ //# sourceMappingURL=in-memory-native-lifecycle.js.map
@@ -5,17 +5,22 @@ import { InMemoryBusinessPages } from './in-memory-business-pages.js';
5
5
  import { InMemoryInstallationLifecycle, } from './in-memory-installation-lifecycle.js';
6
6
  import { InMemoryBusinessInvitations } from './in-memory-invitations.js';
7
7
  import { BusinessMemoryLocks } from './in-memory-locks.js';
8
+ import { InMemoryNativeRecordLifecycle } from './in-memory-native-lifecycle.js';
8
9
  import { InstallationCapacityError, MAX_RETAINED_INSTALLATIONS_PER_ORG, } from './installation-capacity.js';
9
10
  import { migrateLegacyRequestRecord } from './legacy-request-migration.js';
10
- import { activityFromRecord, applyRequestOperation, cloneGrant, cloneRecord, deletedRecord, effectiveInstallation, idempotencyDigest, initialRecord, installationFingerprint, managedInstallationIntentMatches, normalizeInstallationInput, permissionsForBusinessRole, requestFingerprint, validateCollectionEnabled, validateExpectedRevision, validateStoredRecord, } from './model.js';
11
+ import { eraseNativeMemoryRecord, expireNativeMemoryRecord } from './memory-native-retention.js';
12
+ import { activityFromRecord, applyRequestOperation, cloneGrant, cloneRecord, effectiveInstallation, idempotencyDigest, initialRecord, installationFingerprint, managedInstallationIntentMatches, normalizeInstallationInput, permissionsForBusinessRole, requestFingerprint, validateCollectionEnabled, validateExpectedRevision, validateStoredRecord, } from './model.js';
11
13
  import { matchesNativeQueryMetadata, planNativeQuery, runNativeQuery } from './native-query.js';
12
14
  import { commitNativeMemoryRecord, NativeStorageBudget } from './native-storage-budget.js';
13
15
  import { afterCursor, compareRecords, decodeCursor, grantKey, installationIdKey, page, recordKey, scopeKey, } from './pagination.js';
14
16
  import { BusinessPrincipalAuthority, } from './principal-authority.js';
17
+ import { BusinessStaffAuthority } from './staff-authority.js';
15
18
  import { boundedExportPageSize, boundedPageSize, validateEmail, validateScalar, validateScope, } from './validation.js';
16
19
  /** Process-local development/test adapter. Hosted services must use the PostgreSQL adapter. */
17
20
  export class InMemoryBusinessInformationStore {
21
+ staff = new BusinessStaffAuthority((scope, subject) => this.getGrant(scope, subject));
18
22
  pages;
23
+ nativeLifecycle;
19
24
  #installations = new Map();
20
25
  #installationIds = new Map();
21
26
  #publicIds = new Map();
@@ -39,9 +44,19 @@ export class InMemoryBusinessInformationStore {
39
44
  this.#publicId = options.publicId ?? (() => `sol_${randomUUID().replaceAll('-', '')}`);
40
45
  this.#managedDefinition = options.managedDefinition;
41
46
  this.pages = new InMemoryBusinessPages(this, this.#locks, this.#principals, this.#now);
47
+ this.nativeLifecycle = new InMemoryNativeRecordLifecycle({
48
+ installations: this.#installations,
49
+ records: this.#records,
50
+ locks: this.#locks,
51
+ staff: this.staff,
52
+ principals: this.#principals,
53
+ budget: this.#custody,
54
+ now: this.#now,
55
+ });
42
56
  this.#lifecycle = new InMemoryInstallationLifecycle({
43
57
  installations: this.#installations,
44
- getGrant: (scope, subject) => this.#grants.get(grantKey(scope, subject)),
58
+ staff: this.staff,
59
+ principals: this.#principals,
45
60
  withLock: (key, operation) => this.#locks.run(key, operation),
46
61
  now: this.#now,
47
62
  managedDefinition: this.#managedDefinition,
@@ -58,8 +73,8 @@ export class InMemoryBusinessInformationStore {
58
73
  configurePrincipalAuthority(provider) {
59
74
  this.#principals.configure(provider);
60
75
  }
61
- async listEligibleAssignees(scope) {
62
- return this.#principals.eligible(await this.listGrants(scope));
76
+ async listEligibleAssignees(scope, actor) {
77
+ return this.staff.eligibleAssignees(scope, actor, async () => this.#principals.eligible(await this.listGrants(scope)));
63
78
  }
64
79
  getBusinessNotice(scope) {
65
80
  return this.#lifecycle.getBusinessNotice(scope);
@@ -400,11 +415,8 @@ export class InMemoryBusinessInformationStore {
400
415
  return { ok: false, reason: 'invalid_transition', currentRevision: current.revision };
401
416
  }
402
417
  if (input.operation.kind === 'assign' && input.operation.assigneeSubject !== undefined) {
403
- const assignee = this.#grants.get(grantKey(input.scope, input.operation.assigneeSubject));
404
- if (assignee === undefined ||
405
- assignee.revokedAt !== undefined ||
406
- assignee.role === 'viewer' ||
407
- !(await this.#principals.allows(assignee.subject))) {
418
+ if (!(await this.staff.allows(input.scope, input.operation.assigneeSubject, 'records:assign')) ||
419
+ !(await this.#principals.allows(input.operation.assigneeSubject))) {
408
420
  return { ok: false, reason: 'invalid_assignee', currentRevision: current.revision };
409
421
  }
410
422
  }
@@ -495,6 +507,7 @@ export class InMemoryBusinessInformationStore {
495
507
  const candidates = [...this.#records.entries()]
496
508
  .filter(([key, record]) => (scope === undefined || key.startsWith(`${scope}\0`)) &&
497
509
  record.deletedAt === undefined &&
510
+ record.retentionExpiresAt !== null &&
498
511
  record.retentionExpiresAt <= now.toISOString())
499
512
  .slice(0, limit);
500
513
  let purged = 0;
@@ -510,34 +523,17 @@ export class InMemoryBusinessInformationStore {
510
523
  const now = this.#now().toISOString();
511
524
  return [...this.#records.entries()]
512
525
  .filter(([key, record]) => key.startsWith(prefix) &&
513
- (record.deletedAt !== undefined || record.retentionExpiresAt > now))
526
+ (record.deletedAt !== undefined ||
527
+ record.retentionExpiresAt === null ||
528
+ record.retentionExpiresAt > now))
514
529
  .map(([, record]) => record)
515
530
  .sort(compareRecords);
516
531
  }
517
532
  #expireRecordIfNeeded(key, now) {
518
- const current = this.#records.get(key);
519
- if (current === undefined ||
520
- current.deletedAt !== undefined ||
521
- current.retentionExpiresAt > now.toISOString()) {
522
- return current;
523
- }
524
- const erased = deletedRecord(current, 'system:retention', now, 'retention_expired');
525
- this.#commitRecord(key, erased, 'retention_expired');
526
- return erased;
533
+ return expireNativeMemoryRecord({ records: this.#records, commit: this.#commitRecord.bind(this) }, key, now);
527
534
  }
528
535
  #eraseRecord(key, expectedRevision, actorSubject, reason) {
529
- const current = reason === 'retention_expired'
530
- ? this.#records.get(key)
531
- : this.#expireRecordIfNeeded(key, this.#now());
532
- if (current === undefined || current.deletedAt !== undefined) {
533
- return { ok: false, reason: 'not_found', currentRevision: current?.revision ?? 0 };
534
- }
535
- if (current.revision !== expectedRevision) {
536
- return { ok: false, reason: 'conflict', currentRevision: current.revision };
537
- }
538
- const erased = deletedRecord(current, actorSubject, this.#now(), reason);
539
- this.#commitRecord(key, erased, reason === 'customer_request' ? 'deleted' : 'retention_expired');
540
- return { ok: true, record: cloneRecord(erased) };
536
+ return eraseNativeMemoryRecord({ records: this.#records, commit: this.#commitRecord.bind(this) }, key, expectedRevision, actorSubject, reason, this.#now());
541
537
  }
542
538
  #commitRecord(key, record, kind) {
543
539
  commitNativeMemoryRecord(this.#records, this.#activities, this.#custody, key, record, activityFromRecord(record, kind));