enigma-memory 0.1.13 → 0.1.15

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 (32) hide show
  1. package/README.md +36 -17
  2. package/apps/cli/bin/enigma.mjs +3126 -2848
  3. package/deploy/docker-compose.local-production-simulation.yml +12 -10
  4. package/docs/benchmark-attestation-network.md +487 -487
  5. package/docs/benchmark-reproducibility.md +228 -227
  6. package/docs/demo-proof-network.md +275 -275
  7. package/docs/developer-ecosystem.md +223 -223
  8. package/docs/developer-proof-quickstart.md +325 -325
  9. package/docs/enigma-memory-ready-conformance.md +376 -376
  10. package/docs/hosted-cloud-product.md +10 -0
  11. package/docs/install-anywhere.md +34 -17
  12. package/docs/installers-and-desktop.md +9 -7
  13. package/docs/proof-network-build-notes.md +240 -240
  14. package/docs/proof-network.md +257 -257
  15. package/docs/sdk-api.md +324 -324
  16. package/docs/solana-devnet-acceptance.md +48 -0
  17. package/docs/solana-proof-rail.md +453 -453
  18. package/examples/ci/github-actions.yml +6 -8
  19. package/package.json +272 -265
  20. package/packages/mcp-server/src/index.js +1185 -1185
  21. package/packages/passport/src/index.js +9 -5
  22. package/scripts/build-benchmark-proof-release.mjs +391 -0
  23. package/scripts/build-goal-completion-audit.mjs +11 -5
  24. package/scripts/build-hosted-api-key-lifecycle.mjs +274 -274
  25. package/scripts/build-hosted-customer-lifecycle.mjs +456 -456
  26. package/scripts/build-installer-assets.mjs +389 -273
  27. package/scripts/build-production-handoff-packet.mjs +7 -6
  28. package/scripts/build-production-unblocker.mjs +409 -0
  29. package/scripts/build-proof-network-packet.mjs +213 -213
  30. package/scripts/release-audit.mjs +71 -2
  31. package/scripts/run-standard-memory-benchmarks.mjs +1070 -1070
  32. package/scripts/wait-for-backend-ready.mjs +4 -2
@@ -1,456 +1,456 @@
1
- #!/usr/bin/env node
2
- import { writeFile } from 'node:fs/promises';
3
- import { fileURLToPath } from 'node:url';
4
- import { resolve } from 'node:path';
5
-
6
- import {
7
- HOSTED_CLOUD_API_KEY_SCHEMA,
8
- HOSTED_CLOUD_BACKUP_DRILL_SCHEMA,
9
- HOSTED_CLOUD_DASHBOARD_SCHEMA,
10
- HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA,
11
- HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES,
12
- HOSTED_CLOUD_EXTERNAL_BLOCKERS,
13
- HOSTED_CLOUD_INCIDENT_SLA_SCHEMA,
14
- HOSTED_CLOUD_TENANT_SCHEMA,
15
- HOSTED_CLOUD_USAGE_BILLING_SCHEMA,
16
- HOSTED_CLOUD_USER_ACCOUNT_SCHEMA,
17
- HOSTED_CLOUD_VAULT_SCHEMA,
18
- buildApiKeyContract,
19
- buildBackupDrillContract,
20
- buildDashboardSummary,
21
- buildHostedVaultContract,
22
- buildIncidentSlaRefs,
23
- buildTenantContract,
24
- buildUsageBillingRecord,
25
- buildUserAccountContract,
26
- buildCustomerLifecyclePacket,
27
- validateCustomerLifecyclePacket,
28
- } from 'enigma-memory/hosted-cloud';
29
-
30
- export const HOSTED_CUSTOMER_LIFECYCLE_PACKET_SCHEMA = HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA;
31
- export const HOSTED_CUSTOMER_LIFECYCLE_RELEASE_TARGET = '0.1.13';
32
-
33
- const PROVIDED = 'provided';
34
- const BLOCKED_MISSING = 'blocked_missing_evidence';
35
- const BLOCKED_EXTERNAL = 'blocked_external_dependency';
36
- const ALLOWED_STATUSES = new Set([PROVIDED, BLOCKED_MISSING, BLOCKED_EXTERNAL]);
37
- const STATUS_ALIASES = Object.freeze({
38
- blocked: BLOCKED_MISSING,
39
- missing: BLOCKED_MISSING,
40
- external: BLOCKED_EXTERNAL,
41
- });
42
-
43
- const CONTRACT_EVIDENCE = Object.freeze([
44
- {
45
- key: 'user_account',
46
- label: 'Hosted user account contract',
47
- schema: HOSTED_CLOUD_USER_ACCOUNT_SCHEMA,
48
- contractKey: 'user_account',
49
- idField: 'account_id',
50
- },
51
- {
52
- key: 'tenant',
53
- label: 'Hosted tenant contract',
54
- schema: HOSTED_CLOUD_TENANT_SCHEMA,
55
- contractKey: 'tenant',
56
- idField: 'tenant_id',
57
- },
58
- {
59
- key: 'hosted_vault',
60
- label: 'Hosted vault contract',
61
- schema: HOSTED_CLOUD_VAULT_SCHEMA,
62
- contractKey: 'hosted_vault',
63
- idField: 'vault_id',
64
- },
65
- {
66
- key: 'api_key_metadata',
67
- label: 'API key metadata contract',
68
- schema: HOSTED_CLOUD_API_KEY_SCHEMA,
69
- contractKey: 'api_key_metadata',
70
- idField: 'api_key_id',
71
- },
72
- {
73
- key: 'billing',
74
- label: 'Usage billing contract',
75
- schema: HOSTED_CLOUD_USAGE_BILLING_SCHEMA,
76
- contractKey: 'billing',
77
- idField: 'billing_record_id',
78
- },
79
- {
80
- key: 'dashboard',
81
- label: 'Dashboard summary contract',
82
- schema: HOSTED_CLOUD_DASHBOARD_SCHEMA,
83
- contractKey: 'dashboard',
84
- idField: 'dashboard_id',
85
- },
86
- {
87
- key: 'backup_drill',
88
- label: 'Backup drill contract',
89
- schema: HOSTED_CLOUD_BACKUP_DRILL_SCHEMA,
90
- contractKey: 'backup_drill',
91
- idField: 'backup_drill_id',
92
- },
93
- {
94
- key: 'incident_sla_refs',
95
- label: 'Incident and SLA refs contract',
96
- schema: HOSTED_CLOUD_INCIDENT_SLA_SCHEMA,
97
- contractKey: 'incident_sla_refs',
98
- idField: 'incident_sla_id',
99
- },
100
- ]);
101
- const CONTRACT_EVIDENCE_KEYS = new Set(CONTRACT_EVIDENCE.map(({ key }) => key));
102
- const EXTERNAL_BLOCKER_KEYS = new Set(HOSTED_CLOUD_EXTERNAL_BLOCKERS);
103
- const ADDITIONAL_LIFECYCLE_EVIDENCE_KEYS = Object.freeze(['monitoring']);
104
- const EVIDENCE_KEY_ALIASES = Object.freeze({
105
- account: 'user_account',
106
- vault: 'hosted_vault',
107
- api_key: 'api_key_metadata',
108
- backup: 'backup_drill',
109
- incident_sla: 'incident_sla_refs',
110
- support: 'support_ownership',
111
- legal: 'legal_docs',
112
- security_review: 'external_security_review',
113
- });
114
- export const HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS = Object.freeze([
115
- ...CONTRACT_EVIDENCE.map(({ key }) => key),
116
- ...HOSTED_CLOUD_EXTERNAL_BLOCKERS,
117
- ...ADDITIONAL_LIFECYCLE_EVIDENCE_KEYS,
118
- ]);
119
- const LIFECYCLE_EVIDENCE_KEY_SET = new Set([
120
- ...HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS,
121
- ...Object.keys(EVIDENCE_KEY_ALIASES),
122
- ]);
123
-
124
- const SECRET_VALUE_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|https?:\/\/[^\s/@]+:[^\s/@]+@|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|\b(?:raw memory|plaintext prompts?|plain text prompts?|private prompts?|provider responses?|full transcript|decrypted memory|credentials?|secrets?|passwords?|private keys?|api key secret|api secrets?|access tokens?|refresh tokens?|token values?|credential material)\b)/iu;
125
- const FORBIDDEN_CLAIM_RE = /(?:token\s+(?:roi|profit|return|investment|price)|financial\s+roi|roi\s+claim|(?:roi|profit|return)\s+(?:from|on)\s+token|guaranteed\s+(?:savings|profit|return)|provider(?:-side|\s+side)?\s+(?:deletion|erasure)|model\s+(?:forgetting|forgot|erasure)|makes?\s+models?\s+forget|deleted\s+from\s+every\s+provider)/iu;
126
-
127
- function assertPublicSafeString(value, label) {
128
- if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} must be a non-empty public ref`);
129
- if (SECRET_VALUE_RE.test(value) || FORBIDDEN_CLAIM_RE.test(value)) {
130
- throw new Error(`${label} contains non-public hosted-cloud material`);
131
- }
132
- return value.trim();
133
- }
134
-
135
- function normalizeStatus(status, key) {
136
- const normalized = STATUS_ALIASES[status] ?? status;
137
- if (!ALLOWED_STATUSES.has(normalized)) throw new Error('evidence status must be provided, blocked_missing_evidence, or blocked_external_dependency');
138
- if (EXTERNAL_BLOCKER_KEYS.has(key) && normalized === BLOCKED_MISSING) return BLOCKED_EXTERNAL;
139
- if (CONTRACT_EVIDENCE_KEYS.has(key) && normalized === BLOCKED_EXTERNAL) return BLOCKED_MISSING;
140
- return normalized;
141
- }
142
-
143
- function defaultEvidenceRef(key) {
144
- const status = EXTERNAL_BLOCKER_KEYS.has(key) ? BLOCKED_EXTERNAL : BLOCKED_MISSING;
145
- return {
146
- key,
147
- status,
148
- ref: `blocked:${key}`,
149
- blocker: EXTERNAL_BLOCKER_KEYS.has(key)
150
- ? `External ${key.replaceAll('_', ' ')} evidence is not provided.`
151
- : `Public-safe ${key.replaceAll('_', ' ')} evidence is not provided.`,
152
- };
153
- }
154
-
155
- function normalizeEvidenceRecord(key, record) {
156
- const status = normalizeStatus(record.status ?? PROVIDED, key);
157
- const ref = assertPublicSafeString(record.ref, 'evidence ref');
158
- const output = { key, status, ref };
159
- if (status !== PROVIDED) output.blocker = record.blocker ?? defaultEvidenceRef(key).blocker;
160
- return output;
161
- }
162
-
163
- export function parseEvidenceRef(value) {
164
- const raw = assertPublicSafeString(value, 'evidence ref argument');
165
- const equalsIndex = raw.indexOf('=');
166
- if (equalsIndex <= 0 || equalsIndex === raw.length - 1) throw new Error('evidence refs must use key=status:ref');
167
- const rawKey = raw.slice(0, equalsIndex).trim();
168
- const key = EVIDENCE_KEY_ALIASES[rawKey] ?? rawKey;
169
- if (!LIFECYCLE_EVIDENCE_KEY_SET.has(rawKey) || !HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.includes(key)) throw new Error(`evidence key must be one of: ${HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.join(', ')}`);
170
- const body = raw.slice(equalsIndex + 1).trim();
171
- const colonIndex = body.indexOf(':');
172
- if (colonIndex > 0) {
173
- const possibleStatus = body.slice(0, colonIndex);
174
- if (ALLOWED_STATUSES.has(possibleStatus) || STATUS_ALIASES[possibleStatus]) {
175
- return normalizeEvidenceRecord(key, { status: possibleStatus, ref: body.slice(colonIndex + 1) });
176
- }
177
- }
178
- return normalizeEvidenceRecord(key, { status: PROVIDED, ref: body });
179
- }
180
-
181
- function normalizeEvidenceRefs(records = []) {
182
- const refs = Object.fromEntries(HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.map((key) => [key, defaultEvidenceRef(key)]));
183
- for (const rawRecord of records) {
184
- const parsedRecord = typeof rawRecord === 'string' ? parseEvidenceRef(rawRecord) : rawRecord;
185
- const key = EVIDENCE_KEY_ALIASES[parsedRecord.key] ?? parsedRecord.key;
186
- if (!HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.includes(key)) throw new Error(`evidence key must be one of: ${HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.join(', ')}`);
187
- const record = { ...parsedRecord, key };
188
- refs[key] = normalizeEvidenceRecord(key, record);
189
- }
190
- return refs;
191
- }
192
-
193
- function operatorEvidenceRefsFrom(evidenceRefs) {
194
- return Object.fromEntries(HOSTED_CLOUD_EXTERNAL_BLOCKERS.map((key) => {
195
- const evidence = evidenceRefs[key];
196
- if (evidence.status === PROVIDED) return [key, { status: PROVIDED, ref: evidence.ref }];
197
- return [key, { status: BLOCKED_EXTERNAL, ref: evidence.ref, blocker: evidence.blocker }];
198
- }));
199
- }
200
-
201
- function refFor(evidenceRefs, key) {
202
- return evidenceRefs[key].ref;
203
- }
204
-
205
- function buildContractFragments({ tenantId, generatedAt, evidenceRefs }) {
206
- const operator_evidence_refs = operatorEvidenceRefsFrom(evidenceRefs);
207
- const base = { tenant_id: tenantId, generated_at: generatedAt, operator_evidence_refs };
208
- const tenant = buildTenantContract({
209
- ...base,
210
- tenant_ref: refFor(evidenceRefs, 'tenant'),
211
- policy_ref: refFor(evidenceRefs, 'tenant'),
212
- retention_policy_ref: refFor(evidenceRefs, 'tenant'),
213
- data_residency_ref: refFor(evidenceRefs, 'tenant'),
214
- });
215
- const userAccount = buildUserAccountContract({
216
- ...base,
217
- subject_ref: refFor(evidenceRefs, 'user_account'),
218
- auth_provider_user_ref: refFor(evidenceRefs, 'auth_provider'),
219
- });
220
- const hostedVault = buildHostedVaultContract({
221
- ...base,
222
- vault_ref: refFor(evidenceRefs, 'hosted_vault'),
223
- storage_ref: refFor(evidenceRefs, 'hosted_vault'),
224
- kms_key_ref: refFor(evidenceRefs, 'hosted_vault'),
225
- backup_policy_ref: refFor(evidenceRefs, 'backup_drill'),
226
- retention_policy_ref: refFor(evidenceRefs, 'tenant'),
227
- });
228
- const apiKeyMetadata = buildApiKeyContract({
229
- ...base,
230
- subject_ref: refFor(evidenceRefs, 'user_account'),
231
- key_fingerprint: `fingerprint:${refFor(evidenceRefs, 'api_key_metadata')}`,
232
- issued_at: generatedAt,
233
- rotation_ref: refFor(evidenceRefs, 'api_key_metadata'),
234
- });
235
- const billing = buildUsageBillingRecord({
236
- ...base,
237
- period_start: generatedAt,
238
- period_end: generatedAt,
239
- billing_provider_customer_ref: refFor(evidenceRefs, 'billing_provider'),
240
- usage_aggregate_ref: refFor(evidenceRefs, 'billing'),
241
- metered_event_count: 0,
242
- billable_units: 0,
243
- amount_due_minor_units: 0,
244
- });
245
- const incidentSlaRefs = buildIncidentSlaRefs({
246
- ...base,
247
- incident_response_ref: refFor(evidenceRefs, 'incident_sla_refs'),
248
- sla_policy_ref: refFor(evidenceRefs, 'incident_sla_refs'),
249
- support_owner_ref: refFor(evidenceRefs, 'support_ownership'),
250
- escalation_policy_ref: refFor(evidenceRefs, 'incident_sla_refs'),
251
- status_page_ref: refFor(evidenceRefs, 'incident_sla_refs'),
252
- });
253
- const backupDrill = buildBackupDrillContract({
254
- ...base,
255
- vault_ref: hostedVault.vault_ref,
256
- performed_at: generatedAt,
257
- backup_snapshot_ref: refFor(evidenceRefs, 'backup_drill'),
258
- restore_evidence_ref: refFor(evidenceRefs, 'backup_drill'),
259
- rpo_minutes: 0,
260
- rto_minutes: 0,
261
- });
262
- const dashboard = buildDashboardSummary({
263
- ...base,
264
- account_count: 0,
265
- active_api_key_count: 0,
266
- hosted_vault_count: 0,
267
- billing_period_ref: billing.billing_record_id,
268
- open_incident_count: 0,
269
- backup_drill_ref: backupDrill.backup_drill_id,
270
- incident_sla_ref: incidentSlaRefs.incident_sla_id,
271
- });
272
- return {
273
- user_account: userAccount,
274
- tenant,
275
- hosted_vault: hostedVault,
276
- api_key_metadata: apiKeyMetadata,
277
- billing,
278
- dashboard,
279
- backup_drill: backupDrill,
280
- incident_sla_refs: incidentSlaRefs,
281
- };
282
- }
283
-
284
- function canonicalContractsFrom(contracts) {
285
- return {
286
- account: contracts.user_account,
287
- tenant: contracts.tenant,
288
- vault: contracts.hosted_vault,
289
- api_key: contracts.api_key_metadata,
290
- billing: contracts.billing,
291
- dashboard: contracts.dashboard,
292
- backup: contracts.backup_drill,
293
- incident_sla: contracts.incident_sla_refs,
294
- };
295
- }
296
-
297
- function firstBlockedEvidence(evidenceRefs, keys) {
298
- for (const key of keys) {
299
- if (evidenceRefs[key].status !== PROVIDED) return evidenceRefs[key];
300
- }
301
- return null;
302
- }
303
-
304
- function providedLifecycleEvidence(evidence) {
305
- return { status: PROVIDED, ref: evidence.ref };
306
- }
307
-
308
- function blockedLifecycleEvidence(evidence) {
309
- return {
310
- status: BLOCKED_EXTERNAL,
311
- ref: evidence.ref,
312
- blocker: evidence.blocker,
313
- };
314
- }
315
-
316
- function coreEvidenceFor(evidenceRefs, phase, keys) {
317
- const blocked = firstBlockedEvidence(evidenceRefs, keys);
318
- if (blocked) return blockedLifecycleEvidence(blocked);
319
- return providedLifecycleEvidence(evidenceRefs[keys[0]]);
320
- }
321
-
322
- function coreRequiredEvidenceRefs(evidenceRefs) {
323
- const refs = {
324
- account: coreEvidenceFor(evidenceRefs, 'account', ['user_account', 'auth_provider']),
325
- tenant: coreEvidenceFor(evidenceRefs, 'tenant', ['tenant']),
326
- vault: coreEvidenceFor(evidenceRefs, 'vault', ['hosted_vault']),
327
- api_key: coreEvidenceFor(evidenceRefs, 'api_key', ['api_key_metadata']),
328
- billing: coreEvidenceFor(evidenceRefs, 'billing', ['billing', 'billing_provider']),
329
- dashboard: coreEvidenceFor(evidenceRefs, 'dashboard', ['dashboard']),
330
- backup: coreEvidenceFor(evidenceRefs, 'backup', ['backup_drill']),
331
- incident_sla: coreEvidenceFor(evidenceRefs, 'incident_sla', ['incident_sla_refs']),
332
- support: coreEvidenceFor(evidenceRefs, 'support', ['support_ownership']),
333
- monitoring: coreEvidenceFor(evidenceRefs, 'monitoring', ['monitoring']),
334
- legal: coreEvidenceFor(evidenceRefs, 'legal', ['legal_docs', 'data_processing_terms']),
335
- security_review: coreEvidenceFor(evidenceRefs, 'security_review', ['external_security_review']),
336
- };
337
- return Object.fromEntries(HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES
338
- .filter((phase) => phase !== 'operator_go_live')
339
- .map((phase) => [phase, refs[phase]]));
340
- }
341
-
342
- function scriptBoundary() {
343
- return {
344
- public_safe_packet_only: true,
345
- aggregates_contract_validators_only: true,
346
- deploys_or_calls_external_providers: false,
347
- sensitive_material_written: false,
348
- customer_content_stored: false,
349
- provider_wiring_required_elsewhere: true,
350
- };
351
- }
352
-
353
- export function buildHostedCustomerLifecyclePacket(options = {}) {
354
- const generatedAt = options.generatedAt ?? new Date().toISOString();
355
- const tenantId = assertPublicSafeString(options.tenant ?? options.tenantId ?? 'blocked:tenant', 'tenant');
356
- const domain = assertPublicSafeString(options.domain ?? 'blocked:domain', 'domain');
357
- const environment = assertPublicSafeString(options.environment ?? 'blocked:environment', 'environment');
358
- const operatorGoLiveRef = options.operatorGoLiveRef === undefined
359
- ? null
360
- : assertPublicSafeString(options.operatorGoLiveRef, 'operator go-live ref');
361
- const evidenceRefs = normalizeEvidenceRefs(options.evidenceRefs ?? []);
362
- const contractFragments = buildContractFragments({ tenantId, generatedAt, evidenceRefs });
363
- const packet = buildCustomerLifecyclePacket({
364
- generated_at: generatedAt,
365
- contracts: canonicalContractsFrom(contractFragments),
366
- required_evidence_refs: coreRequiredEvidenceRefs(evidenceRefs),
367
- operator_go_live_ref: operatorGoLiveRef,
368
- });
369
- validateCustomerLifecyclePacket(packet);
370
- return {
371
- ...packet,
372
- release_target: HOSTED_CUSTOMER_LIFECYCLE_RELEASE_TARGET,
373
- tenant_id: tenantId,
374
- domain,
375
- environment,
376
- evidence_refs: Object.fromEntries(Object.entries(evidenceRefs).map(([key, value]) => [key, { ...value }])),
377
- operator_external_evidence_refs: operatorEvidenceRefsFrom(evidenceRefs),
378
- boundary: scriptBoundary(),
379
- };
380
- }
381
-
382
- export function parseArgs(argv) {
383
- const args = {
384
- tenant: undefined,
385
- domain: undefined,
386
- environment: undefined,
387
- operatorGoLiveRef: undefined,
388
- evidenceRefs: [],
389
- out: undefined,
390
- help: false,
391
- };
392
- for (let index = 0; index < argv.length; index += 1) {
393
- const arg = argv[index];
394
- if (arg === '--help' || arg === '-h') {
395
- args.help = true;
396
- continue;
397
- }
398
- const readValue = (name) => {
399
- index += 1;
400
- if (index >= argv.length || argv[index].startsWith('--')) throw new Error(`${name} requires a value`);
401
- return argv[index];
402
- };
403
- if (arg === '--tenant') args.tenant = assertPublicSafeString(readValue('--tenant'), 'tenant');
404
- else if (arg === '--domain') args.domain = assertPublicSafeString(readValue('--domain'), 'domain');
405
- else if (arg === '--environment') args.environment = assertPublicSafeString(readValue('--environment'), 'environment');
406
- else if (arg === '--operator-go-live-ref') args.operatorGoLiveRef = assertPublicSafeString(readValue('--operator-go-live-ref'), 'operator go-live ref');
407
- else if (arg === '--evidence-ref') args.evidenceRefs.push(parseEvidenceRef(readValue('--evidence-ref')));
408
- else if (arg === '--out') args.out = assertPublicSafeString(readValue('--out'), 'output file');
409
- else throw new Error('unknown option; use --help');
410
- }
411
- return args;
412
- }
413
-
414
- export function usage() {
415
- return `Usage: node scripts/build-hosted-customer-lifecycle.mjs [options]
416
-
417
- Build a public-safe hosted customer lifecycle packet. This script validates local contract fragments only; it does not deploy, create accounts, call providers, or write secrets.
418
-
419
- Options:
420
- --tenant <id> Tenant id. Defaults to blocked:tenant.
421
- --domain <domain> Customer domain. Defaults to blocked:domain.
422
- --environment <env> Environment label. Defaults to blocked:environment.
423
- --operator-go-live-ref <ref> Explicit operator approval evidence ref.
424
- --evidence-ref <key=status:ref> Repeatable evidence ref. key=<ref> implies provided.
425
- Status: provided, blocked_missing_evidence, blocked_external_dependency.
426
- Keys: ${HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.join(', ')}
427
- --out <file> Also write the packet JSON to a file.
428
- --help Show this help.
429
- `;
430
- }
431
-
432
- export async function main(argv = process.argv.slice(2)) {
433
- const args = parseArgs(argv);
434
- if (args.help) {
435
- process.stdout.write(usage());
436
- return 0;
437
- }
438
- const packet = buildHostedCustomerLifecyclePacket(args);
439
- const json = `${JSON.stringify(packet, null, 2)}\n`;
440
- if (args.out) {
441
- try {
442
- await writeFile(args.out, json, 'utf8');
443
- } catch {
444
- throw new Error('failed to write lifecycle packet output');
445
- }
446
- }
447
- process.stdout.write(json);
448
- return 0;
449
- }
450
-
451
- if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
452
- main().catch((error) => {
453
- process.stderr.write(`Error: ${error?.message ?? 'failed to build lifecycle packet'}\n`);
454
- process.exitCode = 1;
455
- });
456
- }
1
+ #!/usr/bin/env node
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolve } from 'node:path';
5
+
6
+ import {
7
+ HOSTED_CLOUD_API_KEY_SCHEMA,
8
+ HOSTED_CLOUD_BACKUP_DRILL_SCHEMA,
9
+ HOSTED_CLOUD_DASHBOARD_SCHEMA,
10
+ HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA,
11
+ HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES,
12
+ HOSTED_CLOUD_EXTERNAL_BLOCKERS,
13
+ HOSTED_CLOUD_INCIDENT_SLA_SCHEMA,
14
+ HOSTED_CLOUD_TENANT_SCHEMA,
15
+ HOSTED_CLOUD_USAGE_BILLING_SCHEMA,
16
+ HOSTED_CLOUD_USER_ACCOUNT_SCHEMA,
17
+ HOSTED_CLOUD_VAULT_SCHEMA,
18
+ buildApiKeyContract,
19
+ buildBackupDrillContract,
20
+ buildDashboardSummary,
21
+ buildHostedVaultContract,
22
+ buildIncidentSlaRefs,
23
+ buildTenantContract,
24
+ buildUsageBillingRecord,
25
+ buildUserAccountContract,
26
+ buildCustomerLifecyclePacket,
27
+ validateCustomerLifecyclePacket,
28
+ } from 'enigma-memory/hosted-cloud';
29
+
30
+ export const HOSTED_CUSTOMER_LIFECYCLE_PACKET_SCHEMA = HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA;
31
+ export const HOSTED_CUSTOMER_LIFECYCLE_RELEASE_TARGET = '0.1.15';
32
+
33
+ const PROVIDED = 'provided';
34
+ const BLOCKED_MISSING = 'blocked_missing_evidence';
35
+ const BLOCKED_EXTERNAL = 'blocked_external_dependency';
36
+ const ALLOWED_STATUSES = new Set([PROVIDED, BLOCKED_MISSING, BLOCKED_EXTERNAL]);
37
+ const STATUS_ALIASES = Object.freeze({
38
+ blocked: BLOCKED_MISSING,
39
+ missing: BLOCKED_MISSING,
40
+ external: BLOCKED_EXTERNAL,
41
+ });
42
+
43
+ const CONTRACT_EVIDENCE = Object.freeze([
44
+ {
45
+ key: 'user_account',
46
+ label: 'Hosted user account contract',
47
+ schema: HOSTED_CLOUD_USER_ACCOUNT_SCHEMA,
48
+ contractKey: 'user_account',
49
+ idField: 'account_id',
50
+ },
51
+ {
52
+ key: 'tenant',
53
+ label: 'Hosted tenant contract',
54
+ schema: HOSTED_CLOUD_TENANT_SCHEMA,
55
+ contractKey: 'tenant',
56
+ idField: 'tenant_id',
57
+ },
58
+ {
59
+ key: 'hosted_vault',
60
+ label: 'Hosted vault contract',
61
+ schema: HOSTED_CLOUD_VAULT_SCHEMA,
62
+ contractKey: 'hosted_vault',
63
+ idField: 'vault_id',
64
+ },
65
+ {
66
+ key: 'api_key_metadata',
67
+ label: 'API key metadata contract',
68
+ schema: HOSTED_CLOUD_API_KEY_SCHEMA,
69
+ contractKey: 'api_key_metadata',
70
+ idField: 'api_key_id',
71
+ },
72
+ {
73
+ key: 'billing',
74
+ label: 'Usage billing contract',
75
+ schema: HOSTED_CLOUD_USAGE_BILLING_SCHEMA,
76
+ contractKey: 'billing',
77
+ idField: 'billing_record_id',
78
+ },
79
+ {
80
+ key: 'dashboard',
81
+ label: 'Dashboard summary contract',
82
+ schema: HOSTED_CLOUD_DASHBOARD_SCHEMA,
83
+ contractKey: 'dashboard',
84
+ idField: 'dashboard_id',
85
+ },
86
+ {
87
+ key: 'backup_drill',
88
+ label: 'Backup drill contract',
89
+ schema: HOSTED_CLOUD_BACKUP_DRILL_SCHEMA,
90
+ contractKey: 'backup_drill',
91
+ idField: 'backup_drill_id',
92
+ },
93
+ {
94
+ key: 'incident_sla_refs',
95
+ label: 'Incident and SLA refs contract',
96
+ schema: HOSTED_CLOUD_INCIDENT_SLA_SCHEMA,
97
+ contractKey: 'incident_sla_refs',
98
+ idField: 'incident_sla_id',
99
+ },
100
+ ]);
101
+ const CONTRACT_EVIDENCE_KEYS = new Set(CONTRACT_EVIDENCE.map(({ key }) => key));
102
+ const EXTERNAL_BLOCKER_KEYS = new Set(HOSTED_CLOUD_EXTERNAL_BLOCKERS);
103
+ const ADDITIONAL_LIFECYCLE_EVIDENCE_KEYS = Object.freeze(['monitoring']);
104
+ const EVIDENCE_KEY_ALIASES = Object.freeze({
105
+ account: 'user_account',
106
+ vault: 'hosted_vault',
107
+ api_key: 'api_key_metadata',
108
+ backup: 'backup_drill',
109
+ incident_sla: 'incident_sla_refs',
110
+ support: 'support_ownership',
111
+ legal: 'legal_docs',
112
+ security_review: 'external_security_review',
113
+ });
114
+ export const HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS = Object.freeze([
115
+ ...CONTRACT_EVIDENCE.map(({ key }) => key),
116
+ ...HOSTED_CLOUD_EXTERNAL_BLOCKERS,
117
+ ...ADDITIONAL_LIFECYCLE_EVIDENCE_KEYS,
118
+ ]);
119
+ const LIFECYCLE_EVIDENCE_KEY_SET = new Set([
120
+ ...HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS,
121
+ ...Object.keys(EVIDENCE_KEY_ALIASES),
122
+ ]);
123
+
124
+ const SECRET_VALUE_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|https?:\/\/[^\s/@]+:[^\s/@]+@|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|\b(?:raw memory|plaintext prompts?|plain text prompts?|private prompts?|provider responses?|full transcript|decrypted memory|credentials?|secrets?|passwords?|private keys?|api key secret|api secrets?|access tokens?|refresh tokens?|token values?|credential material)\b)/iu;
125
+ const FORBIDDEN_CLAIM_RE = /(?:token\s+(?:roi|profit|return|investment|price)|financial\s+roi|roi\s+claim|(?:roi|profit|return)\s+(?:from|on)\s+token|guaranteed\s+(?:savings|profit|return)|provider(?:-side|\s+side)?\s+(?:deletion|erasure)|model\s+(?:forgetting|forgot|erasure)|makes?\s+models?\s+forget|deleted\s+from\s+every\s+provider)/iu;
126
+
127
+ function assertPublicSafeString(value, label) {
128
+ if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} must be a non-empty public ref`);
129
+ if (SECRET_VALUE_RE.test(value) || FORBIDDEN_CLAIM_RE.test(value)) {
130
+ throw new Error(`${label} contains non-public hosted-cloud material`);
131
+ }
132
+ return value.trim();
133
+ }
134
+
135
+ function normalizeStatus(status, key) {
136
+ const normalized = STATUS_ALIASES[status] ?? status;
137
+ if (!ALLOWED_STATUSES.has(normalized)) throw new Error('evidence status must be provided, blocked_missing_evidence, or blocked_external_dependency');
138
+ if (EXTERNAL_BLOCKER_KEYS.has(key) && normalized === BLOCKED_MISSING) return BLOCKED_EXTERNAL;
139
+ if (CONTRACT_EVIDENCE_KEYS.has(key) && normalized === BLOCKED_EXTERNAL) return BLOCKED_MISSING;
140
+ return normalized;
141
+ }
142
+
143
+ function defaultEvidenceRef(key) {
144
+ const status = EXTERNAL_BLOCKER_KEYS.has(key) ? BLOCKED_EXTERNAL : BLOCKED_MISSING;
145
+ return {
146
+ key,
147
+ status,
148
+ ref: `blocked:${key}`,
149
+ blocker: EXTERNAL_BLOCKER_KEYS.has(key)
150
+ ? `External ${key.replaceAll('_', ' ')} evidence is not provided.`
151
+ : `Public-safe ${key.replaceAll('_', ' ')} evidence is not provided.`,
152
+ };
153
+ }
154
+
155
+ function normalizeEvidenceRecord(key, record) {
156
+ const status = normalizeStatus(record.status ?? PROVIDED, key);
157
+ const ref = assertPublicSafeString(record.ref, 'evidence ref');
158
+ const output = { key, status, ref };
159
+ if (status !== PROVIDED) output.blocker = record.blocker ?? defaultEvidenceRef(key).blocker;
160
+ return output;
161
+ }
162
+
163
+ export function parseEvidenceRef(value) {
164
+ const raw = assertPublicSafeString(value, 'evidence ref argument');
165
+ const equalsIndex = raw.indexOf('=');
166
+ if (equalsIndex <= 0 || equalsIndex === raw.length - 1) throw new Error('evidence refs must use key=status:ref');
167
+ const rawKey = raw.slice(0, equalsIndex).trim();
168
+ const key = EVIDENCE_KEY_ALIASES[rawKey] ?? rawKey;
169
+ if (!LIFECYCLE_EVIDENCE_KEY_SET.has(rawKey) || !HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.includes(key)) throw new Error(`evidence key must be one of: ${HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.join(', ')}`);
170
+ const body = raw.slice(equalsIndex + 1).trim();
171
+ const colonIndex = body.indexOf(':');
172
+ if (colonIndex > 0) {
173
+ const possibleStatus = body.slice(0, colonIndex);
174
+ if (ALLOWED_STATUSES.has(possibleStatus) || STATUS_ALIASES[possibleStatus]) {
175
+ return normalizeEvidenceRecord(key, { status: possibleStatus, ref: body.slice(colonIndex + 1) });
176
+ }
177
+ }
178
+ return normalizeEvidenceRecord(key, { status: PROVIDED, ref: body });
179
+ }
180
+
181
+ function normalizeEvidenceRefs(records = []) {
182
+ const refs = Object.fromEntries(HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.map((key) => [key, defaultEvidenceRef(key)]));
183
+ for (const rawRecord of records) {
184
+ const parsedRecord = typeof rawRecord === 'string' ? parseEvidenceRef(rawRecord) : rawRecord;
185
+ const key = EVIDENCE_KEY_ALIASES[parsedRecord.key] ?? parsedRecord.key;
186
+ if (!HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.includes(key)) throw new Error(`evidence key must be one of: ${HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.join(', ')}`);
187
+ const record = { ...parsedRecord, key };
188
+ refs[key] = normalizeEvidenceRecord(key, record);
189
+ }
190
+ return refs;
191
+ }
192
+
193
+ function operatorEvidenceRefsFrom(evidenceRefs) {
194
+ return Object.fromEntries(HOSTED_CLOUD_EXTERNAL_BLOCKERS.map((key) => {
195
+ const evidence = evidenceRefs[key];
196
+ if (evidence.status === PROVIDED) return [key, { status: PROVIDED, ref: evidence.ref }];
197
+ return [key, { status: BLOCKED_EXTERNAL, ref: evidence.ref, blocker: evidence.blocker }];
198
+ }));
199
+ }
200
+
201
+ function refFor(evidenceRefs, key) {
202
+ return evidenceRefs[key].ref;
203
+ }
204
+
205
+ function buildContractFragments({ tenantId, generatedAt, evidenceRefs }) {
206
+ const operator_evidence_refs = operatorEvidenceRefsFrom(evidenceRefs);
207
+ const base = { tenant_id: tenantId, generated_at: generatedAt, operator_evidence_refs };
208
+ const tenant = buildTenantContract({
209
+ ...base,
210
+ tenant_ref: refFor(evidenceRefs, 'tenant'),
211
+ policy_ref: refFor(evidenceRefs, 'tenant'),
212
+ retention_policy_ref: refFor(evidenceRefs, 'tenant'),
213
+ data_residency_ref: refFor(evidenceRefs, 'tenant'),
214
+ });
215
+ const userAccount = buildUserAccountContract({
216
+ ...base,
217
+ subject_ref: refFor(evidenceRefs, 'user_account'),
218
+ auth_provider_user_ref: refFor(evidenceRefs, 'auth_provider'),
219
+ });
220
+ const hostedVault = buildHostedVaultContract({
221
+ ...base,
222
+ vault_ref: refFor(evidenceRefs, 'hosted_vault'),
223
+ storage_ref: refFor(evidenceRefs, 'hosted_vault'),
224
+ kms_key_ref: refFor(evidenceRefs, 'hosted_vault'),
225
+ backup_policy_ref: refFor(evidenceRefs, 'backup_drill'),
226
+ retention_policy_ref: refFor(evidenceRefs, 'tenant'),
227
+ });
228
+ const apiKeyMetadata = buildApiKeyContract({
229
+ ...base,
230
+ subject_ref: refFor(evidenceRefs, 'user_account'),
231
+ key_fingerprint: `fingerprint:${refFor(evidenceRefs, 'api_key_metadata')}`,
232
+ issued_at: generatedAt,
233
+ rotation_ref: refFor(evidenceRefs, 'api_key_metadata'),
234
+ });
235
+ const billing = buildUsageBillingRecord({
236
+ ...base,
237
+ period_start: generatedAt,
238
+ period_end: generatedAt,
239
+ billing_provider_customer_ref: refFor(evidenceRefs, 'billing_provider'),
240
+ usage_aggregate_ref: refFor(evidenceRefs, 'billing'),
241
+ metered_event_count: 0,
242
+ billable_units: 0,
243
+ amount_due_minor_units: 0,
244
+ });
245
+ const incidentSlaRefs = buildIncidentSlaRefs({
246
+ ...base,
247
+ incident_response_ref: refFor(evidenceRefs, 'incident_sla_refs'),
248
+ sla_policy_ref: refFor(evidenceRefs, 'incident_sla_refs'),
249
+ support_owner_ref: refFor(evidenceRefs, 'support_ownership'),
250
+ escalation_policy_ref: refFor(evidenceRefs, 'incident_sla_refs'),
251
+ status_page_ref: refFor(evidenceRefs, 'incident_sla_refs'),
252
+ });
253
+ const backupDrill = buildBackupDrillContract({
254
+ ...base,
255
+ vault_ref: hostedVault.vault_ref,
256
+ performed_at: generatedAt,
257
+ backup_snapshot_ref: refFor(evidenceRefs, 'backup_drill'),
258
+ restore_evidence_ref: refFor(evidenceRefs, 'backup_drill'),
259
+ rpo_minutes: 0,
260
+ rto_minutes: 0,
261
+ });
262
+ const dashboard = buildDashboardSummary({
263
+ ...base,
264
+ account_count: 0,
265
+ active_api_key_count: 0,
266
+ hosted_vault_count: 0,
267
+ billing_period_ref: billing.billing_record_id,
268
+ open_incident_count: 0,
269
+ backup_drill_ref: backupDrill.backup_drill_id,
270
+ incident_sla_ref: incidentSlaRefs.incident_sla_id,
271
+ });
272
+ return {
273
+ user_account: userAccount,
274
+ tenant,
275
+ hosted_vault: hostedVault,
276
+ api_key_metadata: apiKeyMetadata,
277
+ billing,
278
+ dashboard,
279
+ backup_drill: backupDrill,
280
+ incident_sla_refs: incidentSlaRefs,
281
+ };
282
+ }
283
+
284
+ function canonicalContractsFrom(contracts) {
285
+ return {
286
+ account: contracts.user_account,
287
+ tenant: contracts.tenant,
288
+ vault: contracts.hosted_vault,
289
+ api_key: contracts.api_key_metadata,
290
+ billing: contracts.billing,
291
+ dashboard: contracts.dashboard,
292
+ backup: contracts.backup_drill,
293
+ incident_sla: contracts.incident_sla_refs,
294
+ };
295
+ }
296
+
297
+ function firstBlockedEvidence(evidenceRefs, keys) {
298
+ for (const key of keys) {
299
+ if (evidenceRefs[key].status !== PROVIDED) return evidenceRefs[key];
300
+ }
301
+ return null;
302
+ }
303
+
304
+ function providedLifecycleEvidence(evidence) {
305
+ return { status: PROVIDED, ref: evidence.ref };
306
+ }
307
+
308
+ function blockedLifecycleEvidence(evidence) {
309
+ return {
310
+ status: BLOCKED_EXTERNAL,
311
+ ref: evidence.ref,
312
+ blocker: evidence.blocker,
313
+ };
314
+ }
315
+
316
+ function coreEvidenceFor(evidenceRefs, phase, keys) {
317
+ const blocked = firstBlockedEvidence(evidenceRefs, keys);
318
+ if (blocked) return blockedLifecycleEvidence(blocked);
319
+ return providedLifecycleEvidence(evidenceRefs[keys[0]]);
320
+ }
321
+
322
+ function coreRequiredEvidenceRefs(evidenceRefs) {
323
+ const refs = {
324
+ account: coreEvidenceFor(evidenceRefs, 'account', ['user_account', 'auth_provider']),
325
+ tenant: coreEvidenceFor(evidenceRefs, 'tenant', ['tenant']),
326
+ vault: coreEvidenceFor(evidenceRefs, 'vault', ['hosted_vault']),
327
+ api_key: coreEvidenceFor(evidenceRefs, 'api_key', ['api_key_metadata']),
328
+ billing: coreEvidenceFor(evidenceRefs, 'billing', ['billing', 'billing_provider']),
329
+ dashboard: coreEvidenceFor(evidenceRefs, 'dashboard', ['dashboard']),
330
+ backup: coreEvidenceFor(evidenceRefs, 'backup', ['backup_drill']),
331
+ incident_sla: coreEvidenceFor(evidenceRefs, 'incident_sla', ['incident_sla_refs']),
332
+ support: coreEvidenceFor(evidenceRefs, 'support', ['support_ownership']),
333
+ monitoring: coreEvidenceFor(evidenceRefs, 'monitoring', ['monitoring']),
334
+ legal: coreEvidenceFor(evidenceRefs, 'legal', ['legal_docs', 'data_processing_terms']),
335
+ security_review: coreEvidenceFor(evidenceRefs, 'security_review', ['external_security_review']),
336
+ };
337
+ return Object.fromEntries(HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES
338
+ .filter((phase) => phase !== 'operator_go_live')
339
+ .map((phase) => [phase, refs[phase]]));
340
+ }
341
+
342
+ function scriptBoundary() {
343
+ return {
344
+ public_safe_packet_only: true,
345
+ aggregates_contract_validators_only: true,
346
+ deploys_or_calls_external_providers: false,
347
+ sensitive_material_written: false,
348
+ customer_content_stored: false,
349
+ provider_wiring_required_elsewhere: true,
350
+ };
351
+ }
352
+
353
+ export function buildHostedCustomerLifecyclePacket(options = {}) {
354
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
355
+ const tenantId = assertPublicSafeString(options.tenant ?? options.tenantId ?? 'blocked:tenant', 'tenant');
356
+ const domain = assertPublicSafeString(options.domain ?? 'blocked:domain', 'domain');
357
+ const environment = assertPublicSafeString(options.environment ?? 'blocked:environment', 'environment');
358
+ const operatorGoLiveRef = options.operatorGoLiveRef === undefined
359
+ ? null
360
+ : assertPublicSafeString(options.operatorGoLiveRef, 'operator go-live ref');
361
+ const evidenceRefs = normalizeEvidenceRefs(options.evidenceRefs ?? []);
362
+ const contractFragments = buildContractFragments({ tenantId, generatedAt, evidenceRefs });
363
+ const packet = buildCustomerLifecyclePacket({
364
+ generated_at: generatedAt,
365
+ contracts: canonicalContractsFrom(contractFragments),
366
+ required_evidence_refs: coreRequiredEvidenceRefs(evidenceRefs),
367
+ operator_go_live_ref: operatorGoLiveRef,
368
+ });
369
+ validateCustomerLifecyclePacket(packet);
370
+ return {
371
+ ...packet,
372
+ release_target: HOSTED_CUSTOMER_LIFECYCLE_RELEASE_TARGET,
373
+ tenant_id: tenantId,
374
+ domain,
375
+ environment,
376
+ evidence_refs: Object.fromEntries(Object.entries(evidenceRefs).map(([key, value]) => [key, { ...value }])),
377
+ operator_external_evidence_refs: operatorEvidenceRefsFrom(evidenceRefs),
378
+ boundary: scriptBoundary(),
379
+ };
380
+ }
381
+
382
+ export function parseArgs(argv) {
383
+ const args = {
384
+ tenant: undefined,
385
+ domain: undefined,
386
+ environment: undefined,
387
+ operatorGoLiveRef: undefined,
388
+ evidenceRefs: [],
389
+ out: undefined,
390
+ help: false,
391
+ };
392
+ for (let index = 0; index < argv.length; index += 1) {
393
+ const arg = argv[index];
394
+ if (arg === '--help' || arg === '-h') {
395
+ args.help = true;
396
+ continue;
397
+ }
398
+ const readValue = (name) => {
399
+ index += 1;
400
+ if (index >= argv.length || argv[index].startsWith('--')) throw new Error(`${name} requires a value`);
401
+ return argv[index];
402
+ };
403
+ if (arg === '--tenant') args.tenant = assertPublicSafeString(readValue('--tenant'), 'tenant');
404
+ else if (arg === '--domain') args.domain = assertPublicSafeString(readValue('--domain'), 'domain');
405
+ else if (arg === '--environment') args.environment = assertPublicSafeString(readValue('--environment'), 'environment');
406
+ else if (arg === '--operator-go-live-ref') args.operatorGoLiveRef = assertPublicSafeString(readValue('--operator-go-live-ref'), 'operator go-live ref');
407
+ else if (arg === '--evidence-ref') args.evidenceRefs.push(parseEvidenceRef(readValue('--evidence-ref')));
408
+ else if (arg === '--out') args.out = assertPublicSafeString(readValue('--out'), 'output file');
409
+ else throw new Error('unknown option; use --help');
410
+ }
411
+ return args;
412
+ }
413
+
414
+ export function usage() {
415
+ return `Usage: node scripts/build-hosted-customer-lifecycle.mjs [options]
416
+
417
+ Build a public-safe hosted customer lifecycle packet. This script validates local contract fragments only; it does not deploy, create accounts, call providers, or write secrets.
418
+
419
+ Options:
420
+ --tenant <id> Tenant id. Defaults to blocked:tenant.
421
+ --domain <domain> Customer domain. Defaults to blocked:domain.
422
+ --environment <env> Environment label. Defaults to blocked:environment.
423
+ --operator-go-live-ref <ref> Explicit operator approval evidence ref.
424
+ --evidence-ref <key=status:ref> Repeatable evidence ref. key=<ref> implies provided.
425
+ Status: provided, blocked_missing_evidence, blocked_external_dependency.
426
+ Keys: ${HOSTED_CUSTOMER_LIFECYCLE_EVIDENCE_KEYS.join(', ')}
427
+ --out <file> Also write the packet JSON to a file.
428
+ --help Show this help.
429
+ `;
430
+ }
431
+
432
+ export async function main(argv = process.argv.slice(2)) {
433
+ const args = parseArgs(argv);
434
+ if (args.help) {
435
+ process.stdout.write(usage());
436
+ return 0;
437
+ }
438
+ const packet = buildHostedCustomerLifecyclePacket(args);
439
+ const json = `${JSON.stringify(packet, null, 2)}\n`;
440
+ if (args.out) {
441
+ try {
442
+ await writeFile(args.out, json, 'utf8');
443
+ } catch {
444
+ throw new Error('failed to write lifecycle packet output');
445
+ }
446
+ }
447
+ process.stdout.write(json);
448
+ return 0;
449
+ }
450
+
451
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
452
+ main().catch((error) => {
453
+ process.stderr.write(`Error: ${error?.message ?? 'failed to build lifecycle packet'}\n`);
454
+ process.exitCode = 1;
455
+ });
456
+ }