@noodleseed/one 0.161.1 → 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 (64) 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/agent-kit/package.json +1 -1
  21. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  22. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
  23. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -0
  24. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  25. package/node_modules/@noodle-borg/authoring/dist/managed-collection.d.ts +26 -0
  26. package/node_modules/@noodle-borg/authoring/dist/managed-collection.js +20 -0
  27. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
  28. package/node_modules/@noodle-borg/authoring/dist/server.js +2 -0
  29. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-tenant-resources.js +241 -0
  30. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +4 -1
  31. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +10 -0
  32. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +4 -1
  33. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +4 -1
  34. package/node_modules/@noodle-borg/compiler/dist/compile.js +3 -0
  35. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  36. package/node_modules/@noodle-borg/compiler/dist/index.d.ts +2 -1
  37. package/node_modules/@noodle-borg/compiler/dist/index.js +1 -0
  38. package/node_modules/@noodle-borg/compiler/dist/managed-collections.d.ts +25 -0
  39. package/node_modules/@noodle-borg/compiler/dist/managed-collections.js +184 -0
  40. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +14 -0
  41. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +3 -0
  42. package/node_modules/@noodle-borg/service/dist/business-information/contracts.js +5 -0
  43. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +402 -0
  44. package/node_modules/@noodle-borg/service/dist/business-information/model.js +292 -0
  45. package/node_modules/@noodle-borg/service/dist/business-information/pagination.js +50 -0
  46. package/node_modules/@noodle-borg/service/dist/business-information/portable.js +6 -0
  47. package/node_modules/@noodle-borg/service/dist/business-information/profiles.js +121 -0
  48. package/node_modules/@noodle-borg/service/dist/business-information/validation.js +200 -0
  49. package/node_modules/@noodle-borg/service/dist/oauth/store.d.ts +2 -2
  50. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +48 -0
  51. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +92 -0
  52. package/node_modules/@noodle-borg/service/dist/routes/business-information-wire.js +117 -0
  53. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +551 -0
  54. package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +2 -0
  55. package/node_modules/@noodle-borg/service/dist/service.js +25 -0
  56. package/node_modules/@noodle-borg/service/package.json +1 -1
  57. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +583 -0
  58. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +205 -0
  59. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +4 -3
  60. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  61. package/node_modules/@noodle-borg/wire-contracts/dist/knowledge.d.ts +3 -3
  62. package/node_modules/@noodle-borg/wire-contracts/dist/managed-assistant-sponsorship.d.ts +8 -8
  63. package/node_modules/@noodleseed/assistant/package.json +1 -1
  64. package/package.json +2 -2
@@ -0,0 +1,292 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { builtInCollection, builtInProfile, validateProfilePayload } from './profiles.js';
3
+ import { validateEmail, validateManagedPayload, validateRetentionDays, validateRevision, validateScalar, validateScope, } from './validation.js';
4
+ const ROLE_PERMISSIONS = {
5
+ administrator: [
6
+ 'installation:administer',
7
+ 'grants:manage',
8
+ 'records:create',
9
+ 'records:read',
10
+ 'records:update',
11
+ 'records:assign',
12
+ 'records:status',
13
+ 'records:note',
14
+ 'records:delete',
15
+ 'records:export',
16
+ ],
17
+ manager: [
18
+ 'records:create',
19
+ 'records:read',
20
+ 'records:update',
21
+ 'records:assign',
22
+ 'records:status',
23
+ 'records:note',
24
+ 'records:export',
25
+ ],
26
+ operator: [
27
+ 'records:create',
28
+ 'records:read',
29
+ 'records:update',
30
+ 'records:assign',
31
+ 'records:status',
32
+ 'records:note',
33
+ ],
34
+ viewer: ['records:read'],
35
+ };
36
+ export function permissionsForBusinessRole(role) {
37
+ const permissions = ROLE_PERMISSIONS[role];
38
+ if (permissions === undefined)
39
+ throw new Error(`unknown business role "${String(role)}"`);
40
+ return permissions;
41
+ }
42
+ export function businessGrantAllows(grant, permission) {
43
+ return (grant !== undefined &&
44
+ grant.revokedAt === undefined &&
45
+ permissionsForBusinessRole(grant.role).includes(permission));
46
+ }
47
+ export function normalizeInstallationInput(input) {
48
+ const scope = { ...validateScope(input.scope) };
49
+ const profile = builtInProfile(input.profileKey);
50
+ const managedCollections = [...new Set(input.managedCollections)].sort();
51
+ if (managedCollections.length === 0)
52
+ throw new Error('at least one managed collection must be enabled');
53
+ for (const collectionKey of managedCollections)
54
+ builtInCollection(profile.key, collectionKey);
55
+ return {
56
+ scope,
57
+ profileKey: profile.key,
58
+ profileVersion: profile.version,
59
+ managedCollections,
60
+ retentionDays: validateRetentionDays(input.retentionDays),
61
+ actorSubject: validateScalar('actor subject', input.actorSubject, 256),
62
+ ...(input.actorEmail === undefined ? {} : { actorEmail: validateEmail(input.actorEmail) }),
63
+ };
64
+ }
65
+ export function installationFingerprint(input) {
66
+ return digest({
67
+ scope: input.scope,
68
+ profileKey: input.profileKey,
69
+ profileVersion: input.profileVersion,
70
+ managedCollections: input.managedCollections,
71
+ retentionDays: input.retentionDays,
72
+ actorSubject: input.actorSubject,
73
+ actorEmail: input.actorEmail,
74
+ });
75
+ }
76
+ export function requestFingerprint(input) {
77
+ return digest(input);
78
+ }
79
+ export function idempotencyDigest(value) {
80
+ return digest(validateScalar('idempotency key', value, 128));
81
+ }
82
+ export function validateCollectionEnabled(installation, collectionKey) {
83
+ const normalized = validateScalar('collection key', collectionKey, 64);
84
+ if (!installation.managedCollections.includes(normalized)) {
85
+ throw new Error(`managed collection "${normalized}" is not enabled for this installation`);
86
+ }
87
+ return builtInCollection(installation.profileKey, normalized);
88
+ }
89
+ export function initialRecord(input) {
90
+ const profile = validateCollectionEnabled(input.installation, input.collectionKey);
91
+ const payload = validateProfilePayload(input.installation.profileKey, input.collectionKey, input.payload);
92
+ const actor = validateScalar('actor subject', input.actorSubject, 256);
93
+ const id = validateScalar('record id', input.id, 128);
94
+ const createdAt = input.now.toISOString();
95
+ return {
96
+ scope: { ...input.installation.scope },
97
+ collectionKey: profile.key,
98
+ id,
99
+ profileKey: input.installation.profileKey,
100
+ profileVersion: input.installation.profileVersion,
101
+ schemaVersion: profile.schemaVersion,
102
+ schemaDigest: profile.schemaDigest,
103
+ status: 'new',
104
+ origin: normalizeOrigin(input.origin),
105
+ revision: 1,
106
+ retentionExpiresAt: new Date(input.now.getTime() + input.installation.retentionDays * 24 * 60 * 60 * 1000).toISOString(),
107
+ createdAt,
108
+ createdBySubject: actor,
109
+ updatedAt: createdAt,
110
+ updatedBySubject: actor,
111
+ content: { payload, notes: [] },
112
+ };
113
+ }
114
+ export function applyRequestOperation(current, operation, actorSubject, now, noteId) {
115
+ if (current.deletedAt !== undefined || current.content === undefined)
116
+ return undefined;
117
+ const actor = validateScalar('actor subject', actorSubject, 256);
118
+ const revision = current.revision + 1;
119
+ const updatedAt = now.toISOString();
120
+ if (operation.kind === 'update') {
121
+ const payload = validateProfilePayload(current.profileKey, current.collectionKey, operation.payload);
122
+ return {
123
+ record: cloneRecord({
124
+ ...current,
125
+ revision,
126
+ updatedAt,
127
+ updatedBySubject: actor,
128
+ content: { payload, notes: current.content.notes },
129
+ }),
130
+ activityKind: 'updated',
131
+ };
132
+ }
133
+ if (operation.kind === 'assign') {
134
+ const assignee = operation.assigneeSubject === undefined
135
+ ? undefined
136
+ : validateScalar('assignee subject', operation.assigneeSubject, 256);
137
+ const { assigneeSubject: ignoredAssignee, ...withoutAssignee } = current;
138
+ void ignoredAssignee;
139
+ return {
140
+ record: cloneRecord({
141
+ ...withoutAssignee,
142
+ ...(assignee === undefined ? {} : { assigneeSubject: assignee }),
143
+ revision,
144
+ updatedAt,
145
+ updatedBySubject: actor,
146
+ }),
147
+ activityKind: 'assigned',
148
+ };
149
+ }
150
+ if (operation.kind === 'set_status') {
151
+ if (!validStatusTransition(current.status, operation.status))
152
+ return undefined;
153
+ return {
154
+ record: cloneRecord({
155
+ ...current,
156
+ status: operation.status,
157
+ revision,
158
+ updatedAt,
159
+ updatedBySubject: actor,
160
+ }),
161
+ activityKind: 'status_changed',
162
+ };
163
+ }
164
+ const note = validateNote(operation.note);
165
+ if (current.content.notes.length >= 50)
166
+ throw new Error('managed request cannot exceed 50 notes');
167
+ return {
168
+ record: cloneRecord({
169
+ ...current,
170
+ revision,
171
+ updatedAt,
172
+ updatedBySubject: actor,
173
+ content: {
174
+ payload: current.content.payload,
175
+ notes: [
176
+ ...current.content.notes,
177
+ {
178
+ id: validateScalar('note id', noteId(), 128),
179
+ text: note,
180
+ createdAt: updatedAt,
181
+ createdBySubject: actor,
182
+ },
183
+ ],
184
+ },
185
+ }),
186
+ activityKind: 'note_added',
187
+ };
188
+ }
189
+ export function deletedRecord(current, actorSubject, now, reason) {
190
+ const actor = validateScalar('actor subject', actorSubject, 256);
191
+ const deletedAt = now.toISOString();
192
+ const { content: ignoredContent, ...metadata } = current;
193
+ void ignoredContent;
194
+ return cloneRecord({
195
+ ...metadata,
196
+ origin: { kind: current.origin.kind },
197
+ revision: current.revision + 1,
198
+ updatedAt: deletedAt,
199
+ updatedBySubject: actor,
200
+ deletedAt,
201
+ deletionReason: reason,
202
+ });
203
+ }
204
+ export function activityFromRecord(record, kind) {
205
+ return {
206
+ scope: { ...record.scope },
207
+ collectionKey: record.collectionKey,
208
+ recordId: record.id,
209
+ revision: record.revision,
210
+ kind,
211
+ status: record.status,
212
+ ...(record.assigneeSubject === undefined ? {} : { assigneeSubject: record.assigneeSubject }),
213
+ occurredAt: record.updatedAt,
214
+ actorSubject: record.updatedBySubject,
215
+ ...(record.content === undefined ? {} : { content: cloneContent(record.content) }),
216
+ };
217
+ }
218
+ export function cloneRecord(record) {
219
+ return {
220
+ ...record,
221
+ scope: { ...record.scope },
222
+ origin: { ...record.origin },
223
+ ...(record.content === undefined ? {} : { content: cloneContent(record.content) }),
224
+ };
225
+ }
226
+ export function cloneContent(content) {
227
+ return {
228
+ payload: structuredClone(content.payload),
229
+ notes: content.notes.map((note) => ({ ...note })),
230
+ };
231
+ }
232
+ export function cloneInstallation(installation) {
233
+ return {
234
+ ...installation,
235
+ scope: { ...installation.scope },
236
+ managedCollections: [...installation.managedCollections],
237
+ };
238
+ }
239
+ export function cloneGrant(grant) {
240
+ return { ...grant, scope: { ...grant.scope } };
241
+ }
242
+ export function cloneActivity(activity) {
243
+ return {
244
+ ...activity,
245
+ scope: { ...activity.scope },
246
+ ...(activity.content === undefined ? {} : { content: cloneContent(activity.content) }),
247
+ };
248
+ }
249
+ export function validateExpectedRevision(value) {
250
+ return validateRevision(value);
251
+ }
252
+ function normalizeOrigin(origin) {
253
+ const kinds = ['embedded', 'mcp', 'portal', 'api', 'import'];
254
+ if (!kinds.includes(origin.kind))
255
+ throw new Error('unsupported managed request origin');
256
+ return {
257
+ kind: origin.kind,
258
+ ...(origin.reference === undefined
259
+ ? {}
260
+ : { reference: validateScalar('origin reference', origin.reference, 256) }),
261
+ };
262
+ }
263
+ function validateNote(note) {
264
+ const normalized = validateScalar('note', note, 4096);
265
+ validateManagedPayload({ note: normalized });
266
+ return normalized;
267
+ }
268
+ function validStatusTransition(from, to) {
269
+ if (from === to)
270
+ return true;
271
+ if (from === 'closed')
272
+ return false;
273
+ if (from === 'resolved')
274
+ return to === 'in_progress' || to === 'closed';
275
+ return to === 'new' || to === 'in_progress' || to === 'resolved' || to === 'closed';
276
+ }
277
+ function digest(value) {
278
+ return createHash('sha256').update(stableJson(value)).digest('hex');
279
+ }
280
+ function stableJson(value) {
281
+ if (Array.isArray(value))
282
+ return `[${value.map(stableJson).join(',')}]`;
283
+ if (value !== null && typeof value === 'object') {
284
+ return `{${Object.entries(value)
285
+ .filter(([, entry]) => entry !== undefined)
286
+ .sort(([left], [right]) => left.localeCompare(right))
287
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
288
+ .join(',')}}`;
289
+ }
290
+ return JSON.stringify(value);
291
+ }
292
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,50 @@
1
+ import { CursorValidationError, validateScalar } from './validation.js';
2
+ export function scopeKey(scope) {
3
+ return `${scope.org}\0${scope.app}\0${scope.env}\0${scope.installationId}`;
4
+ }
5
+ export function recordKey(scope, collectionKey, id) {
6
+ return `${scopeKey(scope)}\0${collectionKey}\0${id}`;
7
+ }
8
+ export function encodeCursor(value) {
9
+ return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
10
+ }
11
+ export function decodeCursor(cursor, expected) {
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(Buffer.from(validateScalar('cursor', cursor, 2048), 'base64url').toString('utf8'));
15
+ }
16
+ catch {
17
+ throw new CursorValidationError();
18
+ }
19
+ if (!isCursor(parsed))
20
+ throw new CursorValidationError();
21
+ if (parsed.kind !== expected.kind ||
22
+ parsed.scope !== scopeKey(expected.scope) ||
23
+ parsed.collectionKey !== expected.collectionKey) {
24
+ throw new CursorValidationError('cursor does not belong to this request scope');
25
+ }
26
+ return parsed;
27
+ }
28
+ export function afterCursor(record, cursor) {
29
+ return (record.createdAt > cursor.lastCreatedAt ||
30
+ (record.createdAt === cursor.lastCreatedAt && record.id > cursor.lastId));
31
+ }
32
+ export function compareRecords(left, right) {
33
+ return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
34
+ }
35
+ function isCursor(value) {
36
+ if (typeof value !== 'object' || value === null)
37
+ return false;
38
+ const input = value;
39
+ return (input.version === 1 &&
40
+ (input.kind === 'list' || input.kind === 'export') &&
41
+ typeof input.scope === 'string' &&
42
+ typeof input.collectionKey === 'string' &&
43
+ validTimestamp(input.lastCreatedAt) &&
44
+ typeof input.lastId === 'string' &&
45
+ (input.snapshotAt === undefined || validTimestamp(input.snapshotAt)));
46
+ }
47
+ function validTimestamp(value) {
48
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
49
+ }
50
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1,6 @@
1
+ export * from './contracts.js';
2
+ export * from './in-memory-store.js';
3
+ export * from './model.js';
4
+ export * from './profiles.js';
5
+ export * from './validation.js';
6
+ //# sourceMappingURL=portable.js.map
@@ -0,0 +1,121 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { PayloadValidationError, validateManagedPayload, validateScalar } from './validation.js';
3
+ const requestSchema = (required, properties) => ({
4
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
5
+ type: 'object',
6
+ additionalProperties: false,
7
+ required,
8
+ properties,
9
+ });
10
+ function collection(key, labels, schema) {
11
+ return {
12
+ key,
13
+ schemaVersion: 1,
14
+ schemaDigest: createHash('sha256').update(stableJson(schema)).digest('hex'),
15
+ labels,
16
+ schema,
17
+ };
18
+ }
19
+ const travel = collection('travel_requests', { singular: 'Travel request', plural: 'Travel requests', navigation: 'Requests' }, requestSchema(['request_type', 'summary'], {
20
+ request_type: {
21
+ type: 'string',
22
+ enum: ['service', 'cancellation', 'refund', 'recovery'],
23
+ maxLength: 32,
24
+ },
25
+ summary: { type: 'string', minLength: 1, maxLength: 2000 },
26
+ booking_reference: { type: 'string', maxLength: 128 },
27
+ traveler_reference: { type: 'string', maxLength: 128 },
28
+ }));
29
+ const service = collection('service_requests', { singular: 'Service request', plural: 'Service requests', navigation: 'Requests' }, requestSchema(['request_type', 'summary'], {
30
+ request_type: {
31
+ type: 'string',
32
+ enum: ['support', 'access', 'configuration', 'recovery'],
33
+ maxLength: 32,
34
+ },
35
+ summary: { type: 'string', minLength: 1, maxLength: 2000 },
36
+ account_reference: { type: 'string', maxLength: 128 },
37
+ workspace_reference: { type: 'string', maxLength: 128 },
38
+ }));
39
+ const ecommerce = collection('return_requests', { singular: 'Return request', plural: 'Return requests', navigation: 'Returns' }, requestSchema(['order_reference', 'reason'], {
40
+ order_reference: { type: 'string', minLength: 1, maxLength: 128 },
41
+ reason: { type: 'string', minLength: 1, maxLength: 2000 },
42
+ item_reference: { type: 'string', maxLength: 128 },
43
+ requested_resolution: {
44
+ type: 'string',
45
+ enum: ['refund', 'exchange', 'store_credit', 'support'],
46
+ maxLength: 32,
47
+ },
48
+ }));
49
+ const restaurant = collection('guest_requests', { singular: 'Guest request', plural: 'Guest requests', navigation: 'Guest requests' }, requestSchema(['request_type', 'summary'], {
50
+ request_type: {
51
+ type: 'string',
52
+ enum: ['reservation_help', 'order_help', 'feedback', 'recovery'],
53
+ maxLength: 32,
54
+ },
55
+ summary: { type: 'string', minLength: 1, maxLength: 2000 },
56
+ location_reference: { type: 'string', maxLength: 128 },
57
+ reservation_reference: { type: 'string', maxLength: 128 },
58
+ }));
59
+ export const BUILT_IN_SOLUTION_PROFILES = {
60
+ travel: { key: 'travel', version: 1, label: 'Travel', collections: [travel] },
61
+ b2b_saas: { key: 'b2b_saas', version: 1, label: 'B2B SaaS', collections: [service] },
62
+ ecommerce: { key: 'ecommerce', version: 1, label: 'Ecommerce', collections: [ecommerce] },
63
+ restaurant: { key: 'restaurant', version: 1, label: 'Restaurant', collections: [restaurant] },
64
+ };
65
+ export function builtInProfile(key) {
66
+ const profile = BUILT_IN_SOLUTION_PROFILES[key];
67
+ if (profile === undefined)
68
+ throw new Error(`unknown built-in solution profile "${String(key)}"`);
69
+ return profile;
70
+ }
71
+ export function builtInCollection(profileKey, collectionKey) {
72
+ const normalized = validateScalar('collection key', collectionKey, 64);
73
+ const found = builtInProfile(profileKey).collections.find((candidate) => candidate.key === normalized);
74
+ if (found === undefined) {
75
+ throw new Error(`collection "${normalized}" is not declared by profile "${profileKey}"`);
76
+ }
77
+ return found;
78
+ }
79
+ export function validateProfilePayload(profileKey, collectionKey, value) {
80
+ const payload = validateManagedPayload(value);
81
+ const collectionProfile = builtInCollection(profileKey, collectionKey);
82
+ validateJsonSchema(collectionProfile.schema, payload);
83
+ return payload;
84
+ }
85
+ function validateJsonSchema(schema, payload) {
86
+ for (const required of schema.required) {
87
+ if (!(required in payload)) {
88
+ throw new PayloadValidationError('invalid_json', `managed payload requires "${required}"`);
89
+ }
90
+ }
91
+ for (const [key, value] of Object.entries(payload)) {
92
+ const property = schema.properties[key];
93
+ if (property === undefined) {
94
+ throw new PayloadValidationError('invalid_json', `managed payload field "${key}" is undeclared`);
95
+ }
96
+ if (typeof value !== 'string') {
97
+ throw new PayloadValidationError('invalid_json', `managed payload field "${key}" must be a string`);
98
+ }
99
+ if (property.minLength !== undefined && value.length < property.minLength) {
100
+ throw new PayloadValidationError('invalid_json', `managed payload field "${key}" is too short`);
101
+ }
102
+ if (value.length > property.maxLength) {
103
+ throw new PayloadValidationError('invalid_json', `managed payload field "${key}" is too long`);
104
+ }
105
+ if (property.enum !== undefined && !property.enum.includes(value)) {
106
+ throw new PayloadValidationError('invalid_json', `managed payload field "${key}" has an unsupported value`);
107
+ }
108
+ }
109
+ }
110
+ function stableJson(value) {
111
+ if (Array.isArray(value))
112
+ return `[${value.map(stableJson).join(',')}]`;
113
+ if (value !== null && typeof value === 'object') {
114
+ return `{${Object.entries(value)
115
+ .sort(([left], [right]) => left.localeCompare(right))
116
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
117
+ .join(',')}}`;
118
+ }
119
+ return JSON.stringify(value);
120
+ }
121
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1,200 @@
1
+ import { MANAGED_RETENTION_DAYS } from './contracts.js';
2
+ export const MAX_PAYLOAD_BYTES = 32 * 1024;
3
+ export const MAX_PAYLOAD_DEPTH = 6;
4
+ export const MAX_PAYLOAD_PROPERTIES = 128;
5
+ export const MAX_PAYLOAD_STRING_LENGTH = 4096;
6
+ export const MAX_PAYLOAD_ARRAY_ITEMS = 50;
7
+ export const MAX_PAGE_SIZE = 100;
8
+ export const MAX_EXPORT_PAGE_SIZE = 500;
9
+ export const DEFAULT_PAGE_SIZE = 50;
10
+ export const DEFAULT_RETENTION_DAYS = 30;
11
+ export class PayloadValidationError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'PayloadValidationError';
17
+ }
18
+ }
19
+ export class CursorValidationError extends Error {
20
+ constructor(message = 'cursor is invalid') {
21
+ super(message);
22
+ this.name = 'CursorValidationError';
23
+ }
24
+ }
25
+ const PROHIBITED_FIELD_PATTERNS = [
26
+ /^(?:card_?number|credit_?card|debit_?card|pan|cvv|cvc|card_?expiry)$/i,
27
+ /(?:password|passwd|passcode|secret|api_?key|access_?token|refresh_?token|authorization)/i,
28
+ /(?:ssn|social_?security|national_?id|government_?id|passport|driver.?s?_?licen[cs]e)/i,
29
+ /(?:health|medical|diagnosis|patient|biometric|fingerprint|faceprint|voiceprint)/i,
30
+ ];
31
+ const CREDENTIAL_VALUE_PATTERNS = [
32
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/i,
33
+ /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
34
+ /\b(?:sk|ghp|github_pat|xox[baprs])-[_A-Za-z0-9-]{8,}\b/,
35
+ /\b(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[:=]\s*\S+/i,
36
+ ];
37
+ const GOVERNMENT_ID_VALUE_PATTERNS = [
38
+ /\b\d{3}-\d{2}-\d{4}\b/,
39
+ /\b(?:SSN|social security|national ID|passport number|driver'?s? licen[cs]e)\s*[:#-]\s*\S+/i,
40
+ ];
41
+ const HEALTH_BIOMETRIC_VALUE_PATTERNS = [
42
+ /\b(?:medical record|patient record|diagnosis|biometric template|fingerprint template|faceprint|voiceprint)\s*[:#-]/i,
43
+ ];
44
+ export function validateManagedPayload(value) {
45
+ if (!isPlainObject(value)) {
46
+ throw new PayloadValidationError('invalid_json', 'managed payload must be a JSON object');
47
+ }
48
+ let properties = 0;
49
+ const validated = validateValue(value, '$', 1, () => {
50
+ properties += 1;
51
+ if (properties > MAX_PAYLOAD_PROPERTIES) {
52
+ throw new PayloadValidationError('too_many_properties', `managed payload exceeds ${MAX_PAYLOAD_PROPERTIES} properties`);
53
+ }
54
+ });
55
+ const encoded = Buffer.byteLength(JSON.stringify(validated), 'utf8');
56
+ if (encoded > MAX_PAYLOAD_BYTES) {
57
+ throw new PayloadValidationError('payload_too_large', `managed payload exceeds ${MAX_PAYLOAD_BYTES} bytes`);
58
+ }
59
+ return structuredClone(validated);
60
+ }
61
+ function validateValue(value, path, depth, countProperty) {
62
+ if (depth > MAX_PAYLOAD_DEPTH) {
63
+ throw new PayloadValidationError('payload_too_deep', `managed payload exceeds depth ${MAX_PAYLOAD_DEPTH}`);
64
+ }
65
+ if (value === null || typeof value === 'boolean')
66
+ return value;
67
+ if (typeof value === 'number') {
68
+ if (!Number.isFinite(value)) {
69
+ throw new PayloadValidationError('invalid_json', `${path} must be a finite JSON number`);
70
+ }
71
+ return value;
72
+ }
73
+ if (typeof value === 'string') {
74
+ validateString(value, path);
75
+ return value;
76
+ }
77
+ if (Array.isArray(value)) {
78
+ if (value.length > MAX_PAYLOAD_ARRAY_ITEMS) {
79
+ throw new PayloadValidationError('array_too_large', `${path} exceeds ${MAX_PAYLOAD_ARRAY_ITEMS} array items`);
80
+ }
81
+ return value.map((entry, index) => validateValue(entry, `${path}[${index}]`, depth + 1, countProperty));
82
+ }
83
+ if (!isPlainObject(value)) {
84
+ throw new PayloadValidationError('invalid_json', `${path} contains a non-JSON value`);
85
+ }
86
+ const result = {};
87
+ for (const [key, entry] of Object.entries(value)) {
88
+ countProperty();
89
+ if (key.length === 0 || key.length > 64) {
90
+ throw new PayloadValidationError('invalid_json', `${path} contains an invalid property name`);
91
+ }
92
+ if (PROHIBITED_FIELD_PATTERNS.some((pattern) => pattern.test(key))) {
93
+ throw new PayloadValidationError('prohibited_field', `${path}.${key} is a prohibited field`);
94
+ }
95
+ result[key] = validateValue(entry, `${path}.${key}`, depth + 1, countProperty);
96
+ }
97
+ return result;
98
+ }
99
+ function validateString(value, path) {
100
+ if (value.length > MAX_PAYLOAD_STRING_LENGTH) {
101
+ throw new PayloadValidationError('string_too_long', `${path} exceeds ${MAX_PAYLOAD_STRING_LENGTH} string characters`);
102
+ }
103
+ if (looksLikePaymentCard(value)) {
104
+ throw new PayloadValidationError('payment_card_value', `${path} contains payment card data`);
105
+ }
106
+ if (CREDENTIAL_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
107
+ throw new PayloadValidationError('credential_value', `${path} contains credential data`);
108
+ }
109
+ if (GOVERNMENT_ID_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
110
+ throw new PayloadValidationError('government_id_value', `${path} contains government ID data`);
111
+ }
112
+ if (HEALTH_BIOMETRIC_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
113
+ throw new PayloadValidationError('health_biometric_value', `${path} contains health or biometric data`);
114
+ }
115
+ }
116
+ function looksLikePaymentCard(value) {
117
+ const candidates = value.match(/(?:\d[ -]?){13,19}/g) ?? [];
118
+ return candidates.some((candidate) => {
119
+ const digits = candidate.replace(/\D/g, '');
120
+ return digits.length >= 13 && digits.length <= 19 && luhnValid(digits);
121
+ });
122
+ }
123
+ function luhnValid(digits) {
124
+ let sum = 0;
125
+ let double = false;
126
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
127
+ let digit = Number(digits[index]);
128
+ if (double) {
129
+ digit *= 2;
130
+ if (digit > 9)
131
+ digit -= 9;
132
+ }
133
+ sum += digit;
134
+ double = !double;
135
+ }
136
+ return sum % 10 === 0;
137
+ }
138
+ function isPlainObject(value) {
139
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
140
+ return false;
141
+ const prototype = Object.getPrototypeOf(value);
142
+ return prototype === Object.prototype || prototype === null;
143
+ }
144
+ export function validateScalar(label, value, maximum = 128) {
145
+ const normalized = value.trim();
146
+ if (normalized.length === 0 || normalized.length > maximum || /[\0\r\n]/.test(normalized)) {
147
+ throw new Error(`${label} must be between 1 and ${maximum} single-line characters`);
148
+ }
149
+ return normalized;
150
+ }
151
+ export function validateEmail(value) {
152
+ const normalized = validateScalar('business grant email', value, 320).toLowerCase();
153
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
154
+ throw new Error('business grant email is invalid');
155
+ }
156
+ return normalized;
157
+ }
158
+ export function validateScope(scope) {
159
+ validateSlug('organization', scope.org);
160
+ validateSlug('app', scope.app);
161
+ validateSlug('environment', scope.env);
162
+ validateSlug('installation', scope.installationId);
163
+ return scope;
164
+ }
165
+ export function validateSlug(label, value) {
166
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value)) {
167
+ throw new Error(`${label} must be a lowercase slug`);
168
+ }
169
+ return value;
170
+ }
171
+ export function validateRetentionDays(value) {
172
+ const resolved = value ?? DEFAULT_RETENTION_DAYS;
173
+ if (!MANAGED_RETENTION_DAYS.includes(resolved)) {
174
+ throw new Error('retentionDays must be 7, 30, or 90');
175
+ }
176
+ return resolved;
177
+ }
178
+ export function boundedPageSize(value) {
179
+ if (value === undefined)
180
+ return DEFAULT_PAGE_SIZE;
181
+ if (!Number.isInteger(value) || value < 1 || value > MAX_PAGE_SIZE) {
182
+ throw new Error(`limit must be an integer between 1 and ${MAX_PAGE_SIZE}`);
183
+ }
184
+ return value;
185
+ }
186
+ export function boundedExportPageSize(value) {
187
+ if (value === undefined)
188
+ return DEFAULT_PAGE_SIZE;
189
+ if (!Number.isInteger(value) || value < 1 || value > MAX_EXPORT_PAGE_SIZE) {
190
+ throw new Error(`limit must be an integer between 1 and ${MAX_EXPORT_PAGE_SIZE}`);
191
+ }
192
+ return value;
193
+ }
194
+ export function validateRevision(value) {
195
+ if (!Number.isSafeInteger(value) || value < 0) {
196
+ throw new Error('expectedRevision must be a non-negative safe integer');
197
+ }
198
+ return value;
199
+ }
200
+ //# sourceMappingURL=validation.js.map