@noodleseed/one 0.163.0 → 0.165.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 (106) hide show
  1. package/dist/banner.d.ts +6 -0
  2. package/dist/banner.d.ts.map +1 -0
  3. package/dist/banner.js +29 -0
  4. package/dist/banner.js.map +1 -0
  5. package/dist/commands/project-setup.d.ts.map +1 -1
  6. package/dist/commands/project-setup.js +2 -0
  7. package/dist/commands/project-setup.js.map +1 -1
  8. package/dist/commands/solutions-coordination.d.ts +5 -0
  9. package/dist/commands/solutions-coordination.d.ts.map +1 -0
  10. package/dist/commands/solutions-coordination.js +94 -0
  11. package/dist/commands/solutions-coordination.js.map +1 -0
  12. package/dist/commands/solutions-ops.d.ts.map +1 -1
  13. package/dist/commands/solutions-ops.js +8 -4
  14. package/dist/commands/solutions-ops.js.map +1 -1
  15. package/dist/first-run.d.ts.map +1 -1
  16. package/dist/first-run.js +2 -0
  17. package/dist/first-run.js.map +1 -1
  18. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +2 -0
  19. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  20. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +2 -0
  21. package/node_modules/@noodle-borg/agent-kit/dist/index.d.ts +1 -0
  22. package/node_modules/@noodle-borg/agent-kit/dist/index.js +1 -0
  23. package/node_modules/@noodle-borg/agent-kit/dist/plugin-bootstrap-skill.js +10 -0
  24. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +4 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/welcome-wordmark.d.ts +3 -0
  26. package/node_modules/@noodle-borg/agent-kit/dist/welcome-wordmark.js +10 -0
  27. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +4 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.d.ts +4 -0
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -2
  31. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  32. package/node_modules/@noodle-borg/authoring/dist/config.d.ts +8 -2
  33. package/node_modules/@noodle-borg/authoring/dist/config.js +28 -1
  34. package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts +37 -0
  35. package/node_modules/@noodle-borg/authoring/dist/connectors.js +4 -0
  36. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  37. package/node_modules/@noodle-borg/authoring/dist/server.js +1 -0
  38. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-flags.js +83 -0
  39. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +70 -92
  40. package/node_modules/@noodle-borg/compiler/dist/artifact/operation-ref.d.ts +2 -0
  41. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +4 -1
  42. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +4 -1
  43. package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts +7 -0
  44. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  45. package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +15 -2
  46. package/node_modules/@noodle-borg/compiler/dist/nested-operations.js +71 -0
  47. package/node_modules/@noodle-borg/compute/dist/code-connector.js +14 -2
  48. package/node_modules/@noodle-borg/compute/dist/deterministic-helpers.js +138 -0
  49. package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +48 -0
  50. package/node_modules/@noodle-borg/compute/dist/sandbox-eval.js +39 -5
  51. package/node_modules/@noodle-borg/compute/dist/worker-entry.js +7 -0
  52. package/node_modules/@noodle-borg/compute/dist/worker-pool.js +14 -3
  53. package/node_modules/@noodle-borg/connector-defs/dist/compile-http.js +31 -2
  54. package/node_modules/@noodle-borg/connector-defs/dist/compile.d.ts +1 -1
  55. package/node_modules/@noodle-borg/connector-defs/dist/compile.js +20 -0
  56. package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +84 -0
  57. package/node_modules/@noodle-borg/connector-defs/dist/schema.js +44 -1
  58. package/node_modules/@noodle-borg/connector-http/dist/http-connector.d.ts +7 -0
  59. package/node_modules/@noodle-borg/connector-http/dist/http-connector.js +34 -6
  60. package/node_modules/@noodle-borg/connector-http/dist/http-response.d.ts +8 -1
  61. package/node_modules/@noodle-borg/connector-http/dist/http-response.js +19 -10
  62. package/node_modules/@noodle-borg/connector-http/dist/index.d.ts +1 -1
  63. package/node_modules/@noodle-borg/control-plane/dist/contracts.d.ts +2 -0
  64. package/node_modules/@noodle-borg/control-plane/dist/deploy-gates.js +1 -0
  65. package/node_modules/@noodle-borg/module/dist/contract.d.ts +2 -0
  66. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +7 -1
  67. package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.js +8 -0
  68. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +2 -0
  69. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -0
  70. package/node_modules/@noodle-borg/runtime/dist/nested-operation-host.js +25 -0
  71. package/node_modules/@noodle-borg/runtime/dist/operation-coordination-execution.js +50 -0
  72. package/node_modules/@noodle-borg/runtime/dist/operation-coordination.d.ts +34 -0
  73. package/node_modules/@noodle-borg/runtime/dist/operation-evidence.d.ts +4 -0
  74. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +173 -94
  75. package/node_modules/@noodle-borg/service/dist/application-activity.js +152 -18
  76. package/node_modules/@noodle-borg/service/dist/application-runtime-target.js +15 -2
  77. package/node_modules/@noodle-borg/service/dist/business-information/definition-resolver.js +19 -0
  78. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +1 -1
  79. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-blueprints.js +7 -0
  80. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js +11 -1
  81. package/node_modules/@noodle-borg/service/dist/business-information/model.js +2 -2
  82. package/node_modules/@noodle-borg/service/dist/business-information/portable.js +1 -1
  83. package/node_modules/@noodle-borg/service/dist/business-information/profiles.js +37 -1
  84. package/node_modules/@noodle-borg/service/dist/credential-binding-index.js +19 -1
  85. package/node_modules/@noodle-borg/service/dist/deployment-execution.js +1 -1
  86. package/node_modules/@noodle-borg/service/dist/operation-coordination.js +185 -0
  87. package/node_modules/@noodle-borg/service/dist/operation-evidence.js +3 -1
  88. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +48 -53
  89. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +2 -1
  90. package/node_modules/@noodle-borg/service/dist/routes/business-information-installation.js +51 -17
  91. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +6 -4
  92. package/node_modules/@noodle-borg/service/dist/routes/business-information-staff.js +4 -3
  93. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +11 -6
  94. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +14 -1
  95. package/node_modules/@noodle-borg/service/dist/routes/deploy-public-embed.js +7 -2
  96. package/node_modules/@noodle-borg/service/dist/service.js +2 -1
  97. package/node_modules/@noodle-borg/service/dist/solution-installation-activation.js +58 -3
  98. package/node_modules/@noodle-borg/service/package.json +1 -1
  99. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +52 -0
  100. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +6 -0
  101. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  102. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  103. package/node_modules/@noodle-borg/wire-contracts/dist/operation-coordination.d.ts +59 -0
  104. package/node_modules/@noodle-borg/wire-contracts/dist/operation-coordination.js +51 -0
  105. package/node_modules/@noodleseed/assistant/package.json +1 -1
  106. package/package.json +2 -2
@@ -10,6 +10,16 @@ export function managedSolutionManifest(definition) {
10
10
  const collection = definition.collections.find((entry) => entry.key === blueprint.collection);
11
11
  if (collection === undefined)
12
12
  throw new Error('Managed solution executable collection is missing');
13
+ const recordInput = collectionToPublicWire(collection).recordSchema;
14
+ const inputSchema = 'followUpContact' in blueprint
15
+ ? {
16
+ ...recordInput,
17
+ required: [
18
+ ...(Array.isArray(recordInput.required) ? recordInput.required : []),
19
+ 'contact_email',
20
+ ],
21
+ }
22
+ : recordInput;
13
23
  return {
14
24
  manifestVersion: '2',
15
25
  server: {
@@ -46,7 +56,7 @@ export function managedSolutionManifest(definition) {
46
56
  name: blueprint.tool,
47
57
  title: blueprint.title,
48
58
  description: blueprint.description,
49
- inputSchema: collectionToPublicWire(collection).recordSchema,
59
+ inputSchema,
50
60
  outputSchema: RECORD_OPERATION_SIGNATURES.submit_record?.output,
51
61
  annotations: {
52
62
  confirm: true,
@@ -47,13 +47,13 @@ export function businessGrantAllows(grant, permission) {
47
47
  grant.revokedAt === undefined &&
48
48
  permissionsForBusinessRole(grant.role).includes(permission));
49
49
  }
50
- export function normalizeInstallationInput(input) {
50
+ export function normalizeInstallationInput(input, resolveManaged = builtInDefinition) {
51
51
  const scope = { ...validateScope(input.scope) };
52
52
  if ((input.profileKey === undefined) === (input.definition === undefined)) {
53
53
  throw new Error('installation requires exactly one managed profile or private definition');
54
54
  }
55
55
  const definition = input.definition ??
56
- builtInDefinition(input.profileKey);
56
+ resolveManaged(input.profileKey);
57
57
  validateDefinition(definition);
58
58
  const managedCollections = [...new Set(input.managedCollections)].sort();
59
59
  for (const collectionKey of managedCollections) {
@@ -1,6 +1,6 @@
1
1
  export * from './collection-controls.js';
2
2
  export * from './contracts.js';
3
- export * from './definition-resolver.js';
3
+ export { privateDefinitionFromDeployment, } from './definition-resolver.js';
4
4
  export * from './in-memory-store.js';
5
5
  export * from './legacy-request-migration.js';
6
6
  export * from './managed-releases.js';
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { validateJsonSchema as validateSchema } from '@noodle-borg/compiler';
2
3
  import { managedSolutionBlueprint, managedSolutionVariables, } from './managed-solution-blueprints.js';
3
4
  import { PayloadValidationError, validateManagedPayload, validateScalar } from './validation.js';
4
5
  const requestSchema = (required, properties) => ({
@@ -73,6 +74,12 @@ export const BUILT_IN_SOLUTION_PROFILE_RELEASES = {
73
74
  { key: 'travel', version: 1, label: 'Travel', collections: [travelV1] },
74
75
  { key: 'travel', version: 2, label: 'Travel', collections: [travelV2] },
75
76
  { key: 'travel', version: 3, label: 'Travel', collections: [lightweightCollection(travelV2)] },
77
+ {
78
+ key: 'travel',
79
+ version: 4,
80
+ label: 'Travel',
81
+ collections: [followUpCollection(lightweightCollection(travelV2))],
82
+ },
76
83
  ],
77
84
  b2b_saas: [{ key: 'b2b_saas', version: 1, label: 'B2B SaaS', collections: [service] }],
78
85
  ecommerce: [
@@ -92,6 +99,12 @@ export const BUILT_IN_SOLUTION_PROFILE_RELEASES = {
92
99
  label: 'Restaurant',
93
100
  collections: [lightweightCollection(restaurant)],
94
101
  },
102
+ {
103
+ key: 'restaurant',
104
+ version: 3,
105
+ label: 'Restaurant',
106
+ collections: [followUpCollection(lightweightCollection(restaurant))],
107
+ },
95
108
  ],
96
109
  };
97
110
  export const BUILT_IN_SOLUTION_PROFILES = {
@@ -189,7 +202,7 @@ function collectionDefinition(profile) {
189
202
  management: profile.management,
190
203
  publicFields: profile.publicFields ?? [],
191
204
  editableFields: Object.keys(profile.schema.properties),
192
- fields: { status: { label: 'Status' } },
205
+ fields: { status: { label: 'Status' }, ...profile.fields },
193
206
  filterFields: ['status'],
194
207
  sortFields: ['status'],
195
208
  }),
@@ -213,6 +226,26 @@ function lightweightCollection(previous) {
213
226
  publicFields: Object.keys(previous.schema.properties),
214
227
  };
215
228
  }
229
+ /** Optional storage preserves historical records; the new application tool asks for a reply route. */
230
+ function followUpCollection(previous) {
231
+ return {
232
+ ...previous,
233
+ ...collection(previous.key, previous.labels, {
234
+ ...previous.schema,
235
+ properties: {
236
+ ...previous.schema.properties,
237
+ contact_email: { type: 'string', format: 'email', minLength: 3, maxLength: 254 },
238
+ },
239
+ }, previous.schemaVersion + 1),
240
+ publicFields: [...(previous.publicFields ?? []), 'contact_email'],
241
+ fields: {
242
+ contact_email: {
243
+ label: 'Reply email',
244
+ help: 'The address provided by the guest for staff follow-up. This address has not been verified.',
245
+ },
246
+ },
247
+ };
248
+ }
216
249
  function validateJsonSchema(schema, payload) {
217
250
  for (const required of schema.required) {
218
251
  if (!(required in payload)) {
@@ -236,6 +269,9 @@ function validateJsonSchema(schema, payload) {
236
269
  if (property.enum !== undefined && !property.enum.includes(value)) {
237
270
  throw new PayloadValidationError('invalid_json', `managed payload field "${key}" has an unsupported value`);
238
271
  }
272
+ if (property.format !== undefined && validateSchema(property, value).length > 0) {
273
+ throw new PayloadValidationError('invalid_json', `managed payload field "${key}" has an invalid format`);
274
+ }
239
275
  }
240
276
  }
241
277
  function stableJson(value) {
@@ -27,7 +27,10 @@ export function buildCredentialBindingIndex(artifact) {
27
27
  return [];
28
28
  return source.scan.resolved ? [source.scan] : [];
29
29
  });
30
- const operationRefs = [...fulfilments.flatMap(resolvedOperationRefs), ...sourceRefs];
30
+ const operationRefs = nestedReferences([
31
+ ...fulfilments.flatMap(resolvedOperationRefs),
32
+ ...sourceRefs,
33
+ ]);
31
34
  for (const ref of operationRefs) {
32
35
  const credentialBinding = ref.credentialBinding;
33
36
  if (credentialBinding === undefined)
@@ -67,4 +70,19 @@ export function buildCredentialBindingIndex(artifact) {
67
70
  googleWorkloadIdentity: [...googleWorkloadIdentity.values()],
68
71
  };
69
72
  }
73
+ function nestedReferences(roots) {
74
+ const output = [];
75
+ const queue = roots.map((ref) => ({ ref, depth: 0 }));
76
+ for (let index = 0; index < queue.length; index += 1) {
77
+ const entry = queue[index];
78
+ if (!entry)
79
+ continue;
80
+ if (entry.depth > 32 || queue.length > 65536)
81
+ throw new Error('Invalid compiled connector dependency graph');
82
+ output.push(entry.ref);
83
+ for (const child of entry.ref.calls ?? [])
84
+ queue.push({ ref: child, depth: entry.depth + 1 });
85
+ }
86
+ return output;
87
+ }
70
88
  //# sourceMappingURL=credential-binding-index.js.map
@@ -266,7 +266,7 @@ export async function executeAuthorizedDeployment(input, dependencies) {
266
266
  assets: hostedAssets,
267
267
  });
268
268
  }
269
- const embedId = await provisionPublicEmbed(registry, options, tenant, result.deploymentId);
269
+ const embedId = await provisionPublicEmbed(registry, options, tenant, result.deploymentId, input.allowRevokedEmbedReplacement);
270
270
  return { ok: true, result, ...(embedId === undefined ? {} : { embedId }) };
271
271
  }
272
272
  function reject(status, body) {
@@ -0,0 +1,185 @@
1
+ import { createHash, createHmac, randomUUID } from 'node:crypto';
2
+ import { canonicalJson } from '@noodle-borg/compiler';
3
+ /** The same opaque resource identity is used by ordinary dispatch and reviewed custody migrations. */
4
+ export function deriveOperationCoordinationResource(input) {
5
+ if (typeof input.identityKey !== 'string' ||
6
+ input.identityKey.length < 32 ||
7
+ !/^[A-Za-z0-9_-]{1,128}$/.test(input.connectionId) ||
8
+ !/^[A-Za-z0-9_-]{1,80}$/.test(input.namespace) ||
9
+ typeof input.key !== 'string' ||
10
+ input.key.length < 1 ||
11
+ input.key.length > 2048 ||
12
+ ![input.scope.org, input.scope.app, input.scope.env, input.scope.installationId].every((value) => typeof value === 'string' && value.length > 0 && value.length <= 256))
13
+ throw new Error('Invalid coordination resource authority');
14
+ return createHmac('sha256', input.identityKey)
15
+ .update(canonicalJson({
16
+ scope: input.scope,
17
+ connectionId: input.connectionId,
18
+ namespace: input.namespace,
19
+ key: input.key,
20
+ }))
21
+ .digest('hex');
22
+ }
23
+ /** No business payloads, provider-specific logic, automatic retries or time-based unlocks. */
24
+ export function createOperationCoordinationPort(options) {
25
+ if (options.identityKey.length < 32 || !/^[A-Za-z0-9_-]{16,128}$/.test(options.epoch))
26
+ throw new Error('Invalid coordination authority configuration');
27
+ const now = () => options.now?.() ?? Date.now();
28
+ const authority = async (intent, generation) => {
29
+ const current = options.connectionGeneration(intent.connectionId);
30
+ if (!current ||
31
+ (generation !== undefined && current !== generation) ||
32
+ !(await options.authorize()))
33
+ throw new Error('Original operation authority unavailable');
34
+ return current;
35
+ };
36
+ return {
37
+ async acquire(intent) {
38
+ validateIntent(intent);
39
+ const generation = await authority(intent);
40
+ // Deliberately excludes deployment, connection generation and execution epoch: replacing an
41
+ // artifact or reconnecting cannot bypass a held resource. Tenant/app/environment remain isolated.
42
+ const resource = deriveOperationCoordinationResource({
43
+ scope: options.scope,
44
+ connectionId: intent.connectionId,
45
+ namespace: intent.namespace,
46
+ key: intent.key,
47
+ identityKey: options.identityKey,
48
+ });
49
+ const startedAt = now();
50
+ const record = {
51
+ scope: options.scope,
52
+ resource,
53
+ token: randomUUID(),
54
+ epoch: options.epoch,
55
+ generation,
56
+ reference: intent.reference,
57
+ operationDigest: createHash('sha256').update(intent.id).digest('hex'),
58
+ startedAt,
59
+ deadline: startedAt + intent.executionBoundMs,
60
+ state: 'executing',
61
+ };
62
+ const claimed = await options.store.claim(record);
63
+ const previous = claimed.previous;
64
+ const recoverable = !claimed.acquired &&
65
+ previous !== undefined &&
66
+ previous.generation === generation &&
67
+ previous.epoch === options.epoch &&
68
+ (previous.state === 'unknown' || previous.deadline <= startedAt);
69
+ let resolved = false;
70
+ let finished = false;
71
+ return {
72
+ acquired: claimed.acquired,
73
+ ...(recoverable
74
+ ? {
75
+ previous: {
76
+ reference: previous.reference,
77
+ operationDigest: previous.operationDigest,
78
+ },
79
+ }
80
+ : {}),
81
+ async finish(evidence) {
82
+ if (!claimed.acquired || finished)
83
+ return;
84
+ finished = true;
85
+ // No expiry authorizes a retry. Only proven completion or proof of no dispatched effect
86
+ // releases custody; an outer success message without explicit evidence remains unknown.
87
+ if (evidence.outcome === 'completed' || evidence.outcome === 'rejected') {
88
+ await options.store.release(resource, record.token, evidence.outcome);
89
+ }
90
+ else
91
+ await options.store.markUnknown(resource, record.token);
92
+ },
93
+ async resolvePrevious() {
94
+ if (!recoverable || !previous || resolved)
95
+ throw new Error('Prior operation cannot be resolved');
96
+ await authority(intent, generation);
97
+ if (now() > record.deadline)
98
+ throw new Error('Recovery execution expired');
99
+ if (!(await options.store.release(resource, previous.token, 'source_verified')))
100
+ throw new Error('Prior operation changed before resolution');
101
+ resolved = true;
102
+ },
103
+ };
104
+ },
105
+ };
106
+ }
107
+ function validateIntent(intent) {
108
+ if (!/^[a-f0-9]{64}$/.test(intent.id) ||
109
+ !/^[A-Za-z0-9_-]{1,128}$/.test(intent.connectionId) ||
110
+ !/^[A-Za-z0-9_-]{1,80}$/.test(intent.namespace) ||
111
+ typeof intent.key !== 'string' ||
112
+ intent.key.length < 1 ||
113
+ intent.key.length > 2048 ||
114
+ typeof intent.reference !== 'string' ||
115
+ !/^[A-Za-z0-9._:@/-]{1,256}$/.test(intent.reference) ||
116
+ intent.reference.includes('://') ||
117
+ !Number.isSafeInteger(intent.executionBoundMs) ||
118
+ intent.executionBoundMs < 1 ||
119
+ intent.executionBoundMs > 120000)
120
+ throw new Error('Invalid coordination intent');
121
+ }
122
+ /** Explicit local/test composition; hosted production supplies the PostgreSQL implementation. */
123
+ export class InMemoryOperationCoordinationStore {
124
+ now;
125
+ records = new Map();
126
+ receipts = [];
127
+ constructor(now = Date.now) {
128
+ this.now = now;
129
+ }
130
+ async claim(record) {
131
+ const previous = this.records.get(record.resource);
132
+ if (previous && canonicalJson(previous.scope) !== canonicalJson(record.scope))
133
+ throw new Error('Coordination scope mismatch');
134
+ if (previous)
135
+ return { acquired: false, previous: structuredClone(previous) };
136
+ this.records.set(record.resource, structuredClone(record));
137
+ return { acquired: true };
138
+ }
139
+ async markUnknown(resource, token) {
140
+ const record = this.records.get(resource);
141
+ if (record?.token === token)
142
+ this.records.set(resource, { ...record, state: 'unknown' });
143
+ }
144
+ async release(resource, token, resolution) {
145
+ if (!['completed', 'rejected', 'source_verified'].includes(resolution))
146
+ throw new Error('Invalid coordination resolution');
147
+ const record = this.records.get(resource);
148
+ if (!record || record.token !== token)
149
+ return false;
150
+ if (resolution === 'source_verified')
151
+ this.receipts.push({ record: structuredClone(record), resolution, resolvedAt: this.now() });
152
+ return this.records.delete(resource);
153
+ }
154
+ async list(scope, limit = 100, beforeResource) {
155
+ if (!Number.isSafeInteger(limit) ||
156
+ limit < 1 ||
157
+ limit > 100 ||
158
+ (beforeResource !== undefined && !/^[a-f0-9]{64}$/.test(beforeResource)))
159
+ throw new Error('Invalid coordination paging');
160
+ return [...this.records.values()]
161
+ .filter((record) => canonicalJson(record.scope) === canonicalJson(scope) &&
162
+ (!beforeResource || record.resource > beforeResource))
163
+ .sort((a, b) => a.resource.localeCompare(b.resource))
164
+ .slice(0, limit)
165
+ .map((record) => structuredClone(record));
166
+ }
167
+ async resolve(scope, resource, token, review) {
168
+ if (![review.reviewer, review.reason].every((value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 256))
169
+ throw new Error('Invalid coordination review');
170
+ const record = this.records.get(resource);
171
+ if (!record ||
172
+ record.token !== token ||
173
+ canonicalJson(record.scope) !== canonicalJson(scope) ||
174
+ (record.state === 'executing' && record.deadline > this.now()))
175
+ return false;
176
+ this.receipts.push({
177
+ record: structuredClone(record),
178
+ resolution: 'reviewed',
179
+ ...review,
180
+ resolvedAt: this.now(),
181
+ });
182
+ return this.records.delete(resource);
183
+ }
184
+ }
185
+ //# sourceMappingURL=operation-coordination.js.map
@@ -20,13 +20,14 @@ export function createOperationEvidencePort(options) {
20
20
  bound >= days * 86_400_000 ||
21
21
  !(await options.authorize(intent)))
22
22
  throw new Error('Operation evidence authority unavailable');
23
- const connectionId = intent.operation.credentialBinding?.connectionId;
23
+ const connectionId = intent.operation.credentialBinding?.connectionId ?? intent.connectionId;
24
24
  const generation = connectionId === undefined ? undefined : options.connectionGeneration(connectionId);
25
25
  if (connectionId !== undefined && generation === undefined)
26
26
  throw new Error('Original connected account unavailable');
27
27
  const record = {
28
28
  scope: options.scope,
29
29
  id: intent.id,
30
+ ...(intent.parentId === undefined ? {} : { parentId: intent.parentId }),
30
31
  lease: randomUUID(),
31
32
  epoch: options.epoch,
32
33
  deploymentId: options.deploymentId,
@@ -44,6 +45,7 @@ export function createOperationEvidencePort(options) {
44
45
  input: intent.arguments,
45
46
  caller: intent.caller ?? null,
46
47
  revision: intent.executionRevision ?? null,
48
+ parentId: intent.parentId ?? null,
47
49
  generation: generation ?? null,
48
50
  }),
49
51
  startedAt,
@@ -1,83 +1,78 @@
1
1
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
2
- import { ApplicationActivityListResponseSchema, ApplicationActivityPreviewResponseSchema, ApplicationActivitySettingsResponseSchema, ApplicationActivitySettingsSaveRequestSchema, } from '@noodle-borg/wire-contracts';
3
- import { ActivityPolicyError } from '../application-activity.js';
2
+ import { ActivityPolicyError, } from '../application-activity.js';
4
3
  import { businessGrantAllows } from '../business-information/model.js';
5
4
  import { requireIdentity, requireInstallationPermission, } from './business-information.js';
6
- import { parseBusinessPaging } from './business-information-request.js';
7
5
  export async function handleApplicationActivity(req, res, url, ref, deps) {
8
- const settings = url.pathname.endsWith('/activity/settings');
9
- const exporting = url.pathname.endsWith('/activity/export');
10
- const preview = url.pathname.endsWith('/activity/preview');
11
- if (req.method !== 'GET' && !(settings && req.method === 'PATCH')) {
12
- res.setHeader('allow', settings ? 'GET, PATCH' : 'GET');
6
+ const coordination = ref.action === 'coordination';
7
+ const suffix = url.pathname.split('/').at(-1);
8
+ const action = coordination
9
+ ? suffix === 'resolve'
10
+ ? 'coordination-resolve'
11
+ : 'coordination-list'
12
+ : suffix === 'settings'
13
+ ? req.method === 'PATCH'
14
+ ? 'save-settings'
15
+ : 'settings'
16
+ : suffix === 'preview' || suffix === 'export'
17
+ ? suffix
18
+ : 'list';
19
+ const allowed = coordination
20
+ ? suffix === 'resolve'
21
+ ? ['POST']
22
+ : ['GET']
23
+ : suffix === 'settings'
24
+ ? ['GET', 'PATCH']
25
+ : ['GET'];
26
+ if (!allowed.includes(req.method ?? '')) {
27
+ res.setHeader('allow', allowed.join(', '));
13
28
  return sendJson(res, 405, { error: 'method not allowed' });
14
29
  }
15
30
  const identity = await requireIdentity(req, res, deps);
16
31
  if (identity === false)
17
32
  return;
18
- const permission = req.method === 'PATCH'
33
+ const permission = coordination || action === 'save-settings'
19
34
  ? 'installation:administer'
20
- : exporting
35
+ : action === 'export'
21
36
  ? 'records:export'
22
37
  : 'records:read';
23
- const authorized = await requireInstallationPermission(res, ref, identity, permission, deps);
38
+ const authorize = () => requireInstallationPermission(res, ref, identity, permission, deps);
39
+ const authorized = await authorize();
24
40
  if (!authorized)
25
41
  return;
26
- const reply = async (body) => {
27
- if (await requireInstallationPermission(res, ref, identity, permission, deps))
28
- sendJson(res, 200, body);
29
- };
30
42
  res.setHeader('cache-control', 'private, no-store');
31
43
  try {
32
- const allowedQuery = settings || preview ? [] : ['limit', 'cursor'];
33
- for (const key of url.searchParams.keys())
34
- if (!allowedQuery.includes(key) || url.searchParams.getAll(key).length !== 1)
35
- throw new ActivityPolicyError('activity_invalid');
36
44
  if (!deps.activity)
37
- throw new ActivityPolicyError('activity_unavailable');
38
- const canEdit = businessGrantAllows(authorized.grant, 'installation:administer');
39
- if (settings && req.method === 'PATCH') {
40
- const body = await readJsonBody(req, deps.maxBody);
41
- if (!body.ok)
42
- return sendJson(res, body.status, { error: body.error });
43
- const parsed = ApplicationActivitySettingsSaveRequestSchema.safeParse(body.value);
44
- if (!parsed.success)
45
- throw new ActivityPolicyError('activity_invalid');
46
- const data = await deps.activity.save(authorized.scope, parsed.data);
45
+ throw new ActivityPolicyError(coordination ? 'coordination_unavailable' : 'activity_unavailable');
46
+ let body;
47
+ if (req.method !== 'GET') {
48
+ const parsed = await readJsonBody(req, deps.maxBody);
49
+ if (!parsed.ok)
50
+ return sendJson(res, parsed.status, { error: parsed.error });
51
+ body = parsed.value;
52
+ if (!(await authorize()))
53
+ return;
54
+ }
55
+ const result = await deps.activity.project(authorized.scope, action, {
56
+ parameters: [...url.searchParams.entries()],
57
+ body,
58
+ reviewer: identity.subject,
59
+ canEdit: businessGrantAllows(authorized.grant, 'installation:administer'),
60
+ });
61
+ if ('audit' in result)
47
62
  await deps.audit?.emit({
48
- eventType: 'config.activity.retention_changed',
63
+ ...result.audit,
49
64
  org: ref.org,
50
65
  app: authorized.scope.app,
51
66
  env: authorized.scope.env,
52
67
  actorSubject: identity.subject,
53
- details: { retentionDays: data.retentionDays },
54
68
  });
55
- return reply(ApplicationActivitySettingsResponseSchema.parse({ ok: true, data }));
56
- }
57
- if (preview)
58
- return reply(ApplicationActivityPreviewResponseSchema.parse({
59
- ok: true,
60
- data: await deps.activity.preview(authorized.scope),
61
- }));
62
- if (settings)
63
- return reply(ApplicationActivitySettingsResponseSchema.parse({
64
- ok: true,
65
- data: (await deps.activity.settings(authorized.scope, canEdit)).projection,
66
- }));
67
- const paging = parseBusinessPaging(url, 100);
68
- if (!paging.ok)
69
- return sendJson(res, 400, { error: paging.error });
70
- const data = await deps.activity.page(authorized.scope, {
71
- purpose: exporting ? 'export' : 'list',
72
- limit: paging.value.limit ?? 50,
73
- ...(paging.value.cursor === undefined ? {} : { cursor: paging.value.cursor }),
74
- });
75
- return reply(ApplicationActivityListResponseSchema.parse({ ok: true, data }));
69
+ if (await authorize())
70
+ sendJson(res, 200, result.response);
76
71
  }
77
72
  catch (error) {
78
73
  if (!(error instanceof ActivityPolicyError))
79
74
  throw error;
80
- sendJson(res, error.code === 'activity_unavailable' ? 503 : error.code === 'activity_conflict' ? 409 : 400, { code: error.code, error: error.message });
75
+ sendJson(res, error.code.endsWith('_unavailable') ? 503 : error.code.endsWith('_conflict') ? 409 : 400, { code: error.code, error: error.message });
81
76
  }
82
77
  }
83
78
  //# sourceMappingURL=business-information-activity.js.map
@@ -58,7 +58,8 @@ export function dispatchBusinessInformationRoutes(req, res, url, deps) {
58
58
  return false;
59
59
  if (installationRef.action === 'notice')
60
60
  return run(req, res, deps, () => handleBusinessNotice(req, res, installationRef, deps));
61
- if (installationRef.action === 'activity' && installationRef.collection === undefined)
61
+ if ((installationRef.action === 'activity' || installationRef.action === 'coordination') &&
62
+ installationRef.collection === undefined)
62
63
  return run(req, res, deps, () => handleApplicationActivity(req, res, url, installationRef, deps));
63
64
  if (installationRef.action === 'connections')
64
65
  return run(req, res, deps, () => handleApplicationConnections(req, res, installationRef, deps));
@@ -1,24 +1,58 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { builtInDefinition, } from '../business-information/portable.js';
2
+ import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
3
+ import { SolutionInstallationActivateRequestSchema } from '@noodle-borg/wire-contracts';
4
+ import { sendForbidden } from '../http-util.js';
5
+ import { invalid, methodNotAllowed, requireInstallationPermission, } from './business-information.js';
6
+ import { installationToWire } from './business-information-wire.js';
7
+ import { canManageMembers } from './org-admin.js';
3
8
  export function stableInstallationId(org, app, env) {
4
9
  return `ins-${createHash('sha256').update(`${org}\0${app}\0${env}`).digest('hex').slice(0, 24)}`;
5
10
  }
6
- export async function resolveInstallDefinition(selector, installingOrg, resolvePrivateDefinition) {
7
- if (selector.kind === 'managed') {
8
- return builtInDefinition(selector.profileId);
9
- }
10
- if (selector.publisherOrg !== installingOrg ||
11
- selector.publisherOrg === undefined ||
12
- selector.app === undefined ||
13
- selector.environment === undefined ||
14
- selector.deploymentId === undefined) {
15
- return undefined;
16
- }
17
- return resolvePrivateDefinition?.({
18
- publisherOrg: selector.publisherOrg,
19
- app: selector.app,
20
- environment: selector.environment,
21
- deploymentId: selector.deploymentId,
11
+ /** Read current activation authority; intake is an independent operator setting. */
12
+ export async function installationProjection(installation, role, identity, deps) {
13
+ const state = (await deps.readInstallationActivation?.(installation)) ?? 'unavailable';
14
+ const canRetry = state === 'pending' &&
15
+ deps.activateInstallation !== undefined &&
16
+ role === 'administrator' &&
17
+ (await canManageMembers(deps.controlPlane, installation.scope.org, identity));
18
+ return {
19
+ ...installationToWire(installation, role),
20
+ activation: state === 'pending' ? { state, canRetry } : { state, canRetry: false },
21
+ };
22
+ }
23
+ export async function retryInstallationActivation(req, res, ref, identity, deps) {
24
+ if (req.method !== 'POST')
25
+ return methodNotAllowed(res);
26
+ const authorized = await requireInstallationPermission(res, ref, identity, 'installation:administer', deps);
27
+ if (!authorized)
28
+ return;
29
+ if (!(await canManageMembers(deps.controlPlane, ref.org, identity)))
30
+ return sendForbidden(res, 'org owner required');
31
+ const body = await readJsonBody(req, deps.maxBody);
32
+ if (!body.ok)
33
+ return sendJson(res, body.status, { error: body.error });
34
+ const parsed = SolutionInstallationActivateRequestSchema.safeParse(body.value);
35
+ if (!parsed.success)
36
+ return invalid(res, parsed.error);
37
+ const result = await deps.activateInstallation?.({
38
+ installation: authorized.installation,
39
+ actor: identity,
22
40
  });
41
+ if (!result?.ok)
42
+ return sendJson(res, result?.status ?? 503, {
43
+ ok: false,
44
+ code: result?.code ?? 'installation_activation_unavailable',
45
+ error: result?.message ?? 'The installation is saved. Retry activation when the service is ready.',
46
+ installationId: authorized.scope.installationId,
47
+ });
48
+ const installation = await installationProjection(authorized.installation, authorized.grant.role, identity, deps);
49
+ if (installation.activation?.state !== 'ready')
50
+ return sendJson(res, 503, {
51
+ ok: false,
52
+ code: 'installation_activation_unavailable',
53
+ error: 'The installation is saved, but readiness could not be established. Retry activation after the service setup is restored.',
54
+ installationId: authorized.scope.installationId,
55
+ });
56
+ return sendJson(res, 200, { ok: true, data: { installation } });
23
57
  }
24
58
  //# sourceMappingURL=business-information-installation.js.map
@@ -27,7 +27,7 @@ export function parseSolutionInstallationPath(pathname) {
27
27
  ...(connectionAction === undefined ? {} : { connectionAction }),
28
28
  };
29
29
  }
30
- const application = /^\/v1\/orgs\/([^/]+)\/solution-installations\/([^/]+)\/(settings|notice|channels|activity(?:\/(?:settings|export|preview))?)$/.exec(pathname);
30
+ const application = /^\/v1\/orgs\/([^/]+)\/solution-installations\/([^/]+)\/(activate|settings|notice|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);
@@ -36,9 +36,11 @@ export function parseSolutionInstallationPath(pathname) {
36
36
  : {
37
37
  org,
38
38
  installationId,
39
- action: application[3]?.startsWith('activity')
40
- ? 'activity'
41
- : application[3],
39
+ action: application[3]?.startsWith('operations/')
40
+ ? 'coordination'
41
+ : application[3]?.startsWith('activity')
42
+ ? 'activity'
43
+ : application[3],
42
44
  };
43
45
  }
44
46
  let match = /^\/v1\/orgs\/([^/]+)\/solution-installations$/.exec(pathname);