@noodleseed/one 0.161.2 → 0.162.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 (60) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +3 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/catalog.d.ts.map +1 -1
  5. package/dist/commands/catalog.js +1 -0
  6. package/dist/commands/catalog.js.map +1 -1
  7. package/dist/commands/solutions-ops.d.ts +7 -0
  8. package/dist/commands/solutions-ops.d.ts.map +1 -0
  9. package/dist/commands/solutions-ops.js +283 -0
  10. package/dist/commands/solutions-ops.js.map +1 -0
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
  16. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +2 -1
  17. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  18. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +3 -0
  19. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +7 -0
  20. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
  21. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -0
  22. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  23. package/node_modules/@noodle-borg/authoring/dist/managed-collection.d.ts +26 -0
  24. package/node_modules/@noodle-borg/authoring/dist/managed-collection.js +20 -0
  25. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
  26. package/node_modules/@noodle-borg/authoring/dist/server.js +2 -0
  27. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-tenant-resources.js +241 -0
  28. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +4 -1
  29. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +10 -0
  30. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +4 -1
  31. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +4 -1
  32. package/node_modules/@noodle-borg/compiler/dist/compile.js +3 -0
  33. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  34. package/node_modules/@noodle-borg/compiler/dist/index.d.ts +2 -1
  35. package/node_modules/@noodle-borg/compiler/dist/index.js +1 -0
  36. package/node_modules/@noodle-borg/compiler/dist/managed-collections.d.ts +25 -0
  37. package/node_modules/@noodle-borg/compiler/dist/managed-collections.js +184 -0
  38. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +14 -0
  39. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +3 -0
  40. package/node_modules/@noodle-borg/service/dist/business-information/contracts.js +5 -0
  41. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +402 -0
  42. package/node_modules/@noodle-borg/service/dist/business-information/model.js +292 -0
  43. package/node_modules/@noodle-borg/service/dist/business-information/pagination.js +50 -0
  44. package/node_modules/@noodle-borg/service/dist/business-information/portable.js +6 -0
  45. package/node_modules/@noodle-borg/service/dist/business-information/profiles.js +121 -0
  46. package/node_modules/@noodle-borg/service/dist/business-information/validation.js +200 -0
  47. package/node_modules/@noodle-borg/service/dist/oauth/store.d.ts +2 -2
  48. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +48 -0
  49. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +92 -0
  50. package/node_modules/@noodle-borg/service/dist/routes/business-information-wire.js +117 -0
  51. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +551 -0
  52. package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +2 -0
  53. package/node_modules/@noodle-borg/service/dist/service.js +25 -0
  54. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +583 -0
  55. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +205 -0
  56. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +4 -3
  57. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  58. package/node_modules/@noodle-borg/wire-contracts/dist/knowledge.d.ts +3 -3
  59. package/node_modules/@noodle-borg/wire-contracts/dist/managed-assistant-sponsorship.d.ts +8 -8
  60. package/package.json +1 -1
@@ -1119,6 +1119,13 @@ export declare const manifestV2Schema: z.ZodObject<{
1119
1119
  }, z.core.$strict>>;
1120
1120
  }, z.core.$strict>>;
1121
1121
  }, z.core.$strict>>>;
1122
+ collections: z.ZodOptional<z.ZodArray<z.ZodObject<{
1123
+ name: z.ZodString;
1124
+ title: z.ZodString;
1125
+ description: z.ZodString;
1126
+ schemaVersion: z.ZodNumber;
1127
+ recordSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1128
+ }, z.core.$strict>>>;
1122
1129
  }, z.core.$strip>;
1123
1130
  tools: z.ZodArray<z.ZodObject<{
1124
1131
  name: z.ZodString;
@@ -2304,6 +2311,13 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
2304
2311
  }, z.core.$strict>>;
2305
2312
  }, z.core.$strict>>;
2306
2313
  }, z.core.$strict>>>;
2314
+ collections: z.ZodOptional<z.ZodArray<z.ZodObject<{
2315
+ name: z.ZodString;
2316
+ title: z.ZodString;
2317
+ description: z.ZodString;
2318
+ schemaVersion: z.ZodNumber;
2319
+ recordSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
2320
+ }, z.core.$strict>>>;
2307
2321
  }, z.core.$strip>;
2308
2322
  tools: z.ZodArray<z.ZodObject<{
2309
2323
  name: z.ZodString;
@@ -1,5 +1,6 @@
1
1
  import { knowledgeComponentManifestSchema } from '@noodle-borg/knowledge/portable';
2
2
  import { z } from 'zod';
3
+ import { MAX_MANAGED_COLLECTIONS, managedCollectionManifestSchema, } from '../managed-collections.js';
3
4
  import { MAX_COMPILED_WIDGET_HTML_BYTES, MAX_RAW_WIDGET_HTML_BYTES } from '../widget-limits.js';
4
5
  import { agentGuideSchema } from './agent-guide-schema.js';
5
6
  import { serverAuthSchema, serverAuthV2Schema } from './auth-schema.js';
@@ -420,6 +421,8 @@ const serverV2Schema = serverSchema.extend({
420
421
  agentGuide: agentGuideSchema.optional(),
421
422
  /** Customer-owned knowledge components (ADR 0202): Core v2 only. */
422
423
  knowledge: z.array(knowledgeComponentManifestSchema).optional(),
424
+ /** Managed business records: reusable schema intent only, never operator lifecycle state. */
425
+ collections: z.array(managedCollectionManifestSchema).max(MAX_MANAGED_COLLECTIONS).optional(),
423
426
  });
424
427
  const handoffSchema = z
425
428
  .object({
@@ -0,0 +1,5 @@
1
+ export const MANAGED_RETENTION_DAYS = [7, 30, 90];
2
+ export const BUSINESS_ROLES = ['administrator', 'manager', 'operator', 'viewer'];
3
+ export const MANAGED_REQUEST_STATUSES = ['new', 'in_progress', 'resolved', 'closed'];
4
+ export const BUILT_IN_PROFILE_KEYS = ['travel', 'b2b_saas', 'ecommerce', 'restaurant'];
5
+ //# sourceMappingURL=contracts.js.map
@@ -0,0 +1,402 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { activityFromRecord, applyRequestOperation, cloneActivity, cloneGrant, cloneInstallation, cloneRecord, deletedRecord, idempotencyDigest, initialRecord, installationFingerprint, normalizeInstallationInput, permissionsForBusinessRole, requestFingerprint, validateCollectionEnabled, validateExpectedRevision, } from './model.js';
3
+ import { afterCursor, compareRecords, decodeCursor, encodeCursor, recordKey, scopeKey, } from './pagination.js';
4
+ import { boundedExportPageSize, boundedPageSize, validateEmail, validateScalar, validateScope, } from './validation.js';
5
+ /** Process-local development/test adapter. Hosted services must use the PostgreSQL adapter. */
6
+ export class InMemoryBusinessInformationStore {
7
+ #installations = new Map();
8
+ #installationIds = new Map();
9
+ #publicIds = new Map();
10
+ #installationFingerprints = new Map();
11
+ #grants = new Map();
12
+ #records = new Map();
13
+ #activities = new Map();
14
+ #idempotency = new Map();
15
+ #locks = new Map();
16
+ #now;
17
+ #id;
18
+ #publicId;
19
+ constructor(options = {}) {
20
+ this.#now = options.now ?? (() => new Date());
21
+ this.#id = options.id ?? randomUUID;
22
+ this.#publicId = options.publicId ?? (() => `sol_${randomUUID().replaceAll('-', '')}`);
23
+ }
24
+ async createInstallation(input) {
25
+ const normalized = normalizeInstallationInput(input);
26
+ const key = scopeKey(normalized.scope);
27
+ return this.#withLock(`installation:${normalized.scope.org}:${normalized.scope.installationId}`, async () => {
28
+ const existingKey = this.#installationIds.get(installationIdKey(normalized.scope.org, normalized.scope.installationId));
29
+ const existing = existingKey === undefined ? undefined : this.#installations.get(existingKey);
30
+ if (existing !== undefined) {
31
+ return {
32
+ disposition: existingKey === key &&
33
+ this.#installationFingerprints.get(key) === installationFingerprint(normalized)
34
+ ? 'replayed'
35
+ : 'conflict',
36
+ installation: cloneInstallation(existing),
37
+ };
38
+ }
39
+ const now = this.#now().toISOString();
40
+ const installation = {
41
+ scope: { ...normalized.scope },
42
+ publicId: this.#uniquePublicId(),
43
+ profileKey: normalized.profileKey,
44
+ profileVersion: normalized.profileVersion,
45
+ managedCollections: [...normalized.managedCollections],
46
+ retentionDays: normalized.retentionDays,
47
+ revision: 1,
48
+ createdAt: now,
49
+ createdBySubject: normalized.actorSubject,
50
+ updatedAt: now,
51
+ updatedBySubject: normalized.actorSubject,
52
+ };
53
+ this.#installations.set(key, installation);
54
+ this.#installationIds.set(installationIdKey(normalized.scope.org, normalized.scope.installationId), key);
55
+ this.#publicIds.set(installation.publicId, key);
56
+ this.#installationFingerprints.set(key, installationFingerprint(normalized));
57
+ const administrator = {
58
+ scope: { ...normalized.scope },
59
+ subject: normalized.actorSubject,
60
+ ...(normalized.actorEmail === undefined ? {} : { email: normalized.actorEmail }),
61
+ role: 'administrator',
62
+ revision: 1,
63
+ createdAt: now,
64
+ createdBySubject: normalized.actorSubject,
65
+ updatedAt: now,
66
+ updatedBySubject: normalized.actorSubject,
67
+ };
68
+ this.#grants.set(grantKey(normalized.scope, normalized.actorSubject), administrator);
69
+ return { disposition: 'created', installation: cloneInstallation(installation) };
70
+ });
71
+ }
72
+ getInstallation(scope) {
73
+ const found = this.#installations.get(scopeKey(validateScope(scope)));
74
+ return Promise.resolve(found === undefined ? undefined : cloneInstallation(found));
75
+ }
76
+ getInstallationById(org, installationId) {
77
+ const key = this.#installationIds.get(installationIdKey(validateScalar('organization', org, 63), validateScalar('installation', installationId, 63)));
78
+ const found = key === undefined ? undefined : this.#installations.get(key);
79
+ return Promise.resolve(found === undefined ? undefined : cloneInstallation(found));
80
+ }
81
+ resolveInstallationByPublicId(publicId) {
82
+ const key = this.#publicIds.get(validateScalar('public installation id', publicId, 128));
83
+ const found = key === undefined ? undefined : this.#installations.get(key);
84
+ return Promise.resolve(found === undefined ? undefined : cloneInstallation(found));
85
+ }
86
+ listInstallations(org) {
87
+ const normalized = validateScalar('organization', org, 63);
88
+ return Promise.resolve([...this.#installations.values()]
89
+ .filter((installation) => installation.scope.org === normalized)
90
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
91
+ .map(cloneInstallation));
92
+ }
93
+ getGrant(scope, subject) {
94
+ const found = this.#grants.get(grantKey(validateScope(scope), subject));
95
+ return Promise.resolve(found === undefined ? undefined : cloneGrant(found));
96
+ }
97
+ listGrants(scope) {
98
+ const prefix = `${scopeKey(validateScope(scope))}\0`;
99
+ return Promise.resolve([...this.#grants.entries()]
100
+ .filter(([key]) => key.startsWith(prefix))
101
+ .map(([, grant]) => cloneGrant(grant))
102
+ .sort((left, right) => left.subject.localeCompare(right.subject)));
103
+ }
104
+ async setGrant(input) {
105
+ validateExpectedRevision(input.expectedRevision);
106
+ const subject = validateScalar('grant subject', input.subject, 256);
107
+ const actor = validateScalar('actor subject', input.actorSubject, 256);
108
+ permissionsForBusinessRole(input.role);
109
+ const scope = validateScope(input.scope);
110
+ const key = grantKey(scope, subject);
111
+ return this.#withLock(`grants:${scopeKey(scope)}`, async () => {
112
+ if (!(await this.getInstallation(input.scope)))
113
+ return { ok: false, reason: 'not_found', currentRevision: 0 };
114
+ const current = this.#grants.get(key);
115
+ const currentRevision = current?.revision ?? 0;
116
+ if (currentRevision !== input.expectedRevision) {
117
+ return { ok: false, reason: 'conflict', currentRevision };
118
+ }
119
+ if (current?.role === 'administrator' &&
120
+ current.revokedAt === undefined &&
121
+ input.role !== 'administrator' &&
122
+ this.#liveAdministratorCount(scope) === 1) {
123
+ return { ok: false, reason: 'last_administrator', currentRevision };
124
+ }
125
+ const now = this.#now().toISOString();
126
+ const grant = {
127
+ scope: { ...input.scope },
128
+ subject,
129
+ email: validateEmail(input.email),
130
+ role: input.role,
131
+ revision: currentRevision + 1,
132
+ createdAt: current?.createdAt ?? now,
133
+ createdBySubject: current?.createdBySubject ?? actor,
134
+ updatedAt: now,
135
+ updatedBySubject: actor,
136
+ };
137
+ this.#grants.set(key, grant);
138
+ return { ok: true, grant: cloneGrant(grant) };
139
+ });
140
+ }
141
+ async revokeGrant(input) {
142
+ validateExpectedRevision(input.expectedRevision);
143
+ const scope = validateScope(input.scope);
144
+ const key = grantKey(scope, input.subject);
145
+ return this.#withLock(`grants:${scopeKey(scope)}`, async () => {
146
+ const current = this.#grants.get(key);
147
+ if (current === undefined)
148
+ return { ok: false, reason: 'not_found', currentRevision: 0 };
149
+ if (current.revision !== input.expectedRevision) {
150
+ return { ok: false, reason: 'conflict', currentRevision: current.revision };
151
+ }
152
+ if (current.role === 'administrator' &&
153
+ current.revokedAt === undefined &&
154
+ this.#liveAdministratorCount(scope) === 1) {
155
+ return {
156
+ ok: false,
157
+ reason: 'last_administrator',
158
+ currentRevision: current.revision,
159
+ };
160
+ }
161
+ const now = this.#now().toISOString();
162
+ const grant = {
163
+ ...current,
164
+ revision: current.revision + 1,
165
+ updatedAt: now,
166
+ updatedBySubject: validateScalar('actor subject', input.actorSubject, 256),
167
+ revokedAt: now,
168
+ };
169
+ this.#grants.set(key, grant);
170
+ return { ok: true, grant: cloneGrant(grant) };
171
+ });
172
+ }
173
+ async createRequest(input) {
174
+ const idempotencyKey = idempotencyDigest(input.idempotencyKey);
175
+ const installation = await this.#requiredInstallation(input.scope);
176
+ validateCollectionEnabled(installation, input.collectionKey);
177
+ const key = `${scopeKey(input.scope)}\0${input.collectionKey}\0${idempotencyKey}`;
178
+ return this.#withLock(`request-create:${key}`, async () => {
179
+ const candidate = initialRecord({
180
+ installation,
181
+ collectionKey: input.collectionKey,
182
+ id: this.#id(),
183
+ payload: input.payload,
184
+ origin: input.origin,
185
+ actorSubject: input.actorSubject,
186
+ now: this.#now(),
187
+ });
188
+ const fingerprint = requestFingerprint({
189
+ collectionKey: candidate.collectionKey,
190
+ payload: candidate.content?.payload,
191
+ origin: candidate.origin,
192
+ actorSubject: candidate.createdBySubject,
193
+ });
194
+ const replay = this.#idempotency.get(key);
195
+ if (replay !== undefined) {
196
+ const existing = this.#records.get(replay.recordKey);
197
+ if (existing === undefined)
198
+ throw new Error('idempotency record points to a missing request');
199
+ return {
200
+ disposition: replay.fingerprint === fingerprint ? 'replayed' : 'conflict',
201
+ record: cloneRecord(existing),
202
+ };
203
+ }
204
+ const currentKey = recordKey(input.scope, candidate.collectionKey, candidate.id);
205
+ this.#records.set(currentKey, candidate);
206
+ this.#activities.set(currentKey, [activityFromRecord(candidate, 'created')]);
207
+ this.#idempotency.set(key, { fingerprint, recordKey: currentKey });
208
+ return { disposition: 'created', record: cloneRecord(candidate) };
209
+ });
210
+ }
211
+ #liveAdministratorCount(scope) {
212
+ const prefix = `${scopeKey(scope)}\0`;
213
+ return [...this.#grants.entries()].filter(([key, grant]) => key.startsWith(prefix) && grant.role === 'administrator' && grant.revokedAt === undefined).length;
214
+ }
215
+ async getRequest(scope, collectionKey, id, options = {}) {
216
+ const installation = await this.#requiredInstallation(scope);
217
+ const collection = validateCollectionEnabled(installation, collectionKey);
218
+ const record = this.#records.get(recordKey(scope, collection.key, validateScalar('record id', id, 128)));
219
+ if (record === undefined || (record.deletedAt !== undefined && options.includeDeleted !== true))
220
+ return undefined;
221
+ return cloneRecord(record);
222
+ }
223
+ async listRequests(input) {
224
+ const installation = await this.#requiredInstallation(input.scope);
225
+ const collection = validateCollectionEnabled(installation, input.collectionKey);
226
+ const limit = boundedPageSize(input.limit);
227
+ const cursor = input.cursor === undefined
228
+ ? undefined
229
+ : decodeCursor(input.cursor, {
230
+ kind: 'list',
231
+ scope: input.scope,
232
+ collectionKey: collection.key,
233
+ });
234
+ const records = this.#matchingRecords(input.scope, collection.key)
235
+ .filter((record) => input.includeDeleted === true || record.deletedAt === undefined)
236
+ .filter((record) => input.status === undefined || record.status === input.status)
237
+ .filter((record) => input.assigneeSubject === undefined || record.assigneeSubject === input.assigneeSubject)
238
+ .filter((record) => cursor === undefined || afterCursor(record, cursor));
239
+ return page(records, limit, 'list', input.scope, collection.key);
240
+ }
241
+ async mutateRequest(input) {
242
+ validateExpectedRevision(input.expectedRevision);
243
+ const installation = await this.#requiredInstallation(input.scope);
244
+ const collection = validateCollectionEnabled(installation, input.collectionKey);
245
+ const key = recordKey(input.scope, collection.key, validateScalar('record id', input.id, 128));
246
+ return this.#withLock(`request:${key}`, async () => {
247
+ const current = this.#records.get(key);
248
+ if (current === undefined || current.deletedAt !== undefined) {
249
+ return { ok: false, reason: 'not_found', currentRevision: 0 };
250
+ }
251
+ if (current.revision !== input.expectedRevision) {
252
+ return { ok: false, reason: 'conflict', currentRevision: current.revision };
253
+ }
254
+ const applied = applyRequestOperation(current, input.operation, input.actorSubject, this.#now(), this.#id);
255
+ if (applied === undefined) {
256
+ return { ok: false, reason: 'invalid_transition', currentRevision: current.revision };
257
+ }
258
+ this.#records.set(key, applied.record);
259
+ this.#activities.get(key)?.push(activityFromRecord(applied.record, applied.activityKind));
260
+ return { ok: true, record: cloneRecord(applied.record) };
261
+ });
262
+ }
263
+ async deleteRequest(input) {
264
+ validateExpectedRevision(input.expectedRevision);
265
+ const installation = await this.#requiredInstallation(input.scope);
266
+ const collection = validateCollectionEnabled(installation, input.collectionKey);
267
+ const key = recordKey(input.scope, collection.key, validateScalar('record id', input.id, 128));
268
+ return this.#withLock(`request:${key}`, () => Promise.resolve(this.#eraseRecord(key, input.expectedRevision, input.actorSubject, 'customer_request')));
269
+ }
270
+ listActivity(scope, collectionKey, id) {
271
+ const key = recordKey(validateScope(scope), collectionKey, validateScalar('record id', id, 128));
272
+ return Promise.resolve((this.#activities.get(key) ?? []).map(cloneActivity));
273
+ }
274
+ async exportRequests(input) {
275
+ const installation = await this.#requiredInstallation(input.scope);
276
+ const collection = validateCollectionEnabled(installation, input.collectionKey);
277
+ const limit = boundedExportPageSize(input.limit);
278
+ const cursor = input.cursor === undefined
279
+ ? undefined
280
+ : decodeCursor(input.cursor, {
281
+ kind: 'export',
282
+ scope: input.scope,
283
+ collectionKey: collection.key,
284
+ });
285
+ const snapshotAt = cursor?.snapshotAt ?? this.#now().toISOString();
286
+ if (snapshotAt === undefined)
287
+ throw new Error('export cursor is missing its snapshot');
288
+ const records = this.#matchingRecords(input.scope, collection.key)
289
+ .filter((record) => record.createdAt <= snapshotAt)
290
+ .filter((record) => input.includeDeleted === true || record.deletedAt === undefined)
291
+ .filter((record) => cursor === undefined || afterCursor(record, cursor));
292
+ return {
293
+ ...page(records, limit, 'export', input.scope, collection.key, snapshotAt),
294
+ snapshotAt,
295
+ };
296
+ }
297
+ async purgeExpired(input) {
298
+ const limit = boundedPageSize(input.limit);
299
+ const scope = input.scope === undefined ? undefined : scopeKey(validateScope(input.scope));
300
+ const now = this.#now();
301
+ const candidates = [...this.#records.entries()]
302
+ .filter(([key, record]) => (scope === undefined || key.startsWith(`${scope}\0`)) &&
303
+ record.deletedAt === undefined &&
304
+ record.retentionExpiresAt <= now.toISOString())
305
+ .slice(0, limit);
306
+ let purged = 0;
307
+ for (const [key, record] of candidates) {
308
+ const result = await this.#withLock(`request:${key}`, () => Promise.resolve(this.#eraseRecord(key, record.revision, 'system:retention', 'retention_expired')));
309
+ if (result.ok)
310
+ purged += 1;
311
+ }
312
+ return purged;
313
+ }
314
+ #matchingRecords(scope, collectionKey) {
315
+ const prefix = `${scopeKey(scope)}\0${collectionKey}\0`;
316
+ return [...this.#records.entries()]
317
+ .filter(([key]) => key.startsWith(prefix))
318
+ .map(([, record]) => record)
319
+ .sort(compareRecords);
320
+ }
321
+ #eraseRecord(key, expectedRevision, actorSubject, reason) {
322
+ const current = this.#records.get(key);
323
+ if (current === undefined || current.deletedAt !== undefined) {
324
+ return { ok: false, reason: 'not_found', currentRevision: current?.revision ?? 0 };
325
+ }
326
+ if (current.revision !== expectedRevision) {
327
+ return { ok: false, reason: 'conflict', currentRevision: current.revision };
328
+ }
329
+ const erased = deletedRecord(current, actorSubject, this.#now(), reason);
330
+ this.#records.set(key, erased);
331
+ const erasedHistory = (this.#activities.get(key) ?? []).map((activity) => {
332
+ const { content: ignored, ...metadata } = activity;
333
+ void ignored;
334
+ return metadata;
335
+ });
336
+ this.#activities.set(key, [
337
+ ...erasedHistory,
338
+ activityFromRecord(erased, reason === 'customer_request' ? 'deleted' : 'retention_expired'),
339
+ ]);
340
+ return { ok: true, record: cloneRecord(erased) };
341
+ }
342
+ async #requiredInstallation(scope) {
343
+ const installation = await this.getInstallation(scope);
344
+ if (installation === undefined)
345
+ throw new Error('solution installation was not found');
346
+ return installation;
347
+ }
348
+ #uniquePublicId() {
349
+ for (let attempt = 0; attempt < 8; attempt += 1) {
350
+ const candidate = validateScalar('public installation id', this.#publicId(), 128);
351
+ if (!this.#publicIds.has(candidate))
352
+ return candidate;
353
+ }
354
+ throw new Error('could not allocate a unique public installation id');
355
+ }
356
+ async #withLock(key, operation) {
357
+ const previous = this.#locks.get(key) ?? Promise.resolve();
358
+ let release = () => void 0;
359
+ const current = new Promise((resolve) => {
360
+ release = resolve;
361
+ });
362
+ const queued = previous.then(() => current);
363
+ this.#locks.set(key, queued);
364
+ await previous;
365
+ try {
366
+ return await operation();
367
+ }
368
+ finally {
369
+ release(undefined);
370
+ if (this.#locks.get(key) === queued)
371
+ this.#locks.delete(key);
372
+ }
373
+ }
374
+ }
375
+ function installationIdKey(org, installationId) {
376
+ return `${org}\0${installationId}`;
377
+ }
378
+ function grantKey(scope, subject) {
379
+ return `${scopeKey(scope)}\0${validateScalar('grant subject', subject, 256)}`;
380
+ }
381
+ function page(records, limit, kind, scope, collectionKey, snapshotAt) {
382
+ const selected = records.slice(0, limit).map(cloneRecord);
383
+ const last = selected.at(-1);
384
+ const hasMore = records.length > limit;
385
+ return {
386
+ records: selected,
387
+ ...(hasMore && last !== undefined
388
+ ? {
389
+ nextCursor: encodeCursor({
390
+ version: 1,
391
+ kind,
392
+ scope: scopeKey(scope),
393
+ collectionKey,
394
+ lastCreatedAt: last.createdAt,
395
+ lastId: last.id,
396
+ ...(snapshotAt === undefined ? {} : { snapshotAt }),
397
+ }),
398
+ }
399
+ : {}),
400
+ };
401
+ }
402
+ //# sourceMappingURL=in-memory-store.js.map