enigma-memory 0.1.9 → 0.1.11
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.
- package/README.md +30 -7
- package/apps/cli/bin/enigma.mjs +381 -48
- package/docs/developer-ecosystem.md +19 -11
- package/docs/hosted-cloud-product.md +12 -2
- package/docs/sdk-api.md +22 -1
- package/package.json +3 -1
- package/packages/hosted-cloud/src/index.js +420 -2
- package/packages/mcp-server/src/index.js +1 -1
- package/scripts/build-hosted-api-key-lifecycle.mjs +274 -0
- package/scripts/build-hosted-customer-lifecycle.mjs +7 -44
- package/scripts/build-installer-assets.mjs +1 -1
- package/scripts/run-standard-memory-benchmarks.mjs +1 -1
|
@@ -2,30 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
Enigma Memory is a local-first SDK, CLI, MCP server, and service-contract package. The developer surfaces are designed to be copied without secrets, cloud credentials, hidden local paths, or account identifiers.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Public test-drive loop
|
|
6
6
|
|
|
7
7
|
For most developers, start with the installed CLI before reading the SDK internals or service contracts:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install -g enigma-memory
|
|
11
|
-
enigma
|
|
11
|
+
enigma test-drive --overwrite
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`enigma
|
|
14
|
+
`enigma test-drive --overwrite` is zero-credential, local-only, and public-safe by default. It writes an isolated demo under `.enigma/test-drive`, keeps the default bundle inside that directory, emits one JSON summary, and does not print raw private memory plaintext. Use `--dry-run` to preview without writing or `--out-dir <path>` to choose another isolated demo directory.
|
|
15
|
+
|
|
16
|
+
The test drive runs the local proof/demo path only: setup artifacts, search/status output, a cross-model demo report, and benchmark pointers. It does not call external providers, contact hosted Enigma SaaS, require OpenAI/Anthropic/Cloudflare credentials, create accounts, or write third-party client configs. Keep public claims bounded to local Enigma-controlled vault state, receipts, checkpoints, committed roots, exported bundle shape, and declared boundary operations.
|
|
15
17
|
|
|
16
|
-
To
|
|
18
|
+
To explicitly connect real local clients that are already installed or already configured:
|
|
17
19
|
|
|
18
20
|
```sh
|
|
19
|
-
enigma setup --
|
|
21
|
+
enigma setup --connect-installed --overwrite
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
`--connect-installed` implies auto selection, writes only for installed/config-present clients, and skips missing configs instead of creating every default client config. Only explicit write flags mutate client configs; `enigma connect <client>` without `--dry-run` remains the single-client write path, and existing `enigma setup --write-connectors` behavior for explicit/default clients is unchanged. Treat provider-native memory as non-canonical cache only; the local Enigma vault is canonical.
|
|
25
|
+
|
|
26
|
+
## CLI memory passport loop
|
|
27
|
+
|
|
28
|
+
After the public test drive, create a regular local workspace when you want day-to-day CLI usage:
|
|
23
29
|
|
|
24
30
|
```sh
|
|
25
|
-
|
|
31
|
+
npm install -g enigma-memory
|
|
32
|
+
enigma setup --overwrite
|
|
26
33
|
```
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
`enigma setup --overwrite` is the safe default for a regular local workspace. It writes local Enigma artifacts under the workspace `.enigma` path and emits deterministic, public-safe JSON without printing raw memory plaintext. It does not write third-party app configs.
|
|
29
36
|
|
|
30
37
|
After setup, use the same local vault from the CLI or connected clients:
|
|
31
38
|
|
|
@@ -60,15 +67,16 @@ The example app prints ids, counts, roots, and verification status only. It does
|
|
|
60
67
|
|
|
61
68
|
## CLI and CI loop
|
|
62
69
|
|
|
63
|
-
The CI example installs Node 24, installs the published `enigma-memory@0.1.
|
|
70
|
+
The CI example installs Node 24, installs the published `enigma-memory@0.1.11` package, runs:
|
|
64
71
|
|
|
65
72
|
```sh
|
|
73
|
+
npx --yes --package enigma-memory enigma test-drive --overwrite
|
|
66
74
|
npx --yes --package enigma-memory enigma setup --overwrite
|
|
67
75
|
npx --yes --package enigma-memory enigma doctor
|
|
68
76
|
npm run benchmark:memory-suite -- --out benchmark-report.json
|
|
69
77
|
```
|
|
70
78
|
|
|
71
|
-
and then runs a small ESM import smoke. It does not require GitHub secrets, cloud provider credentials, npm tokens, private bundles, local path assumptions, or official dataset network downloads in normal CI. The benchmark
|
|
79
|
+
and then runs a small ESM import smoke. It does not require GitHub secrets, cloud provider credentials, npm tokens, private bundles, local path assumptions, hosted Enigma SaaS, external memory-provider calls, or official dataset network downloads in normal CI. The test-drive and benchmark steps write public-safe local JSON reports; see the benchmark reproducibility guide for claim boundaries and the requirements for any future live third-party comparison.
|
|
72
80
|
|
|
73
81
|
The workflow also includes optional official-dataset benchmark preparation steps gated behind the manual `workflow_dispatch` input `run_standard_benchmark: true`. Normal `push` and `pull_request` runs skip them, so official dataset downloads are not required in normal CI. Enable the manual path only after the repository has reviewed network use and dataset-license handling:
|
|
74
82
|
|
|
@@ -84,7 +92,7 @@ Use the workflow as a template in a consumer repository. It is intentionally lim
|
|
|
84
92
|
|
|
85
93
|
## MCP client loop
|
|
86
94
|
|
|
87
|
-
The same installed package can be used by Claude Desktop, Cursor, Kimi Code, or any generic MCP client. The smooth setup path is `enigma setup --client auto --overwrite` to plan detected clients, then `enigma setup --connect-installed --overwrite` only when you explicitly want setup to write installed/config-present client configs. Manual snippets remain useful when a client needs a copied entry; replace the bundle path with the local path from your setup output, and restart the client.
|
|
95
|
+
The same installed package can be used by Claude Desktop, Cursor, Kimi Code, or any generic MCP client. Run `enigma test-drive --overwrite` first if you want a zero-credential local proof before touching real client configs. The smooth setup path is `enigma setup --client auto --overwrite` to plan detected clients, then `enigma setup --connect-installed --overwrite` only when you explicitly want setup to write installed/config-present client configs. Manual snippets remain useful when a client needs a copied entry; replace the bundle path with the local path from your setup output, and restart the client.
|
|
88
96
|
|
|
89
97
|
Claude Desktop:
|
|
90
98
|
|
|
@@ -10,6 +10,7 @@ The source package now has pure contract builders and validators in `packages/ho
|
|
|
10
10
|
- tenant records;
|
|
11
11
|
- hosted vault records;
|
|
12
12
|
- API key metadata records;
|
|
13
|
+
- API key lifecycle packets for issue/rotate/revoke/audit readiness;
|
|
13
14
|
- usage billing records;
|
|
14
15
|
- dashboard summaries;
|
|
15
16
|
- backup drill records;
|
|
@@ -18,7 +19,7 @@ The source package now has pure contract builders and validators in `packages/ho
|
|
|
18
19
|
|
|
19
20
|
These functions are contract and validation code only. They do not call an auth provider, billing provider, cloud deployment, KMS, backup target, support desk, status page, SIEM, or model provider. They are safe to import as package code because they do not start servers, read user files, mutate deployment state, publish packages, or contact external accounts.
|
|
20
21
|
|
|
21
|
-
The lifecycle packet public
|
|
22
|
+
The lifecycle packet public APIs are `buildCustomerLifecyclePacket(input)` / `validateCustomerLifecyclePacket(packet)` and `buildApiKeyLifecyclePacket(input)` / `validateApiKeyLifecyclePacket(packet)` under `enigma-memory/hosted-cloud`; the script commands below emit the same schemas for release evidence.
|
|
22
23
|
|
|
23
24
|
The validators enforce hosted-cloud boundaries:
|
|
24
25
|
|
|
@@ -27,10 +28,11 @@ The validators enforce hosted-cloud boundaries:
|
|
|
27
28
|
- raw memory, plaintext prompts, provider responses, transcripts, credential-looking values, token values, private keys, and API key secret material are rejected;
|
|
28
29
|
- financial outcome claims, token ROI/profit claims, provider-side deletion claims, and model-forgetting claims are rejected;
|
|
29
30
|
- API key contracts store identifiers, fingerprints, scopes, rotation refs, and timestamps only, not key material;
|
|
31
|
+
- API key lifecycle packets store issue/rotate/revoke/audit evidence refs, event refs, fingerprints, opaque subjects, readiness status, and operator approval refs only; they reject raw key material, provider payloads, customer memory, plaintext prompts, credentials, ROI claims, provider-deletion claims, and model-forgetting claims;
|
|
30
32
|
- hosted vault contracts are opaque-record and plaintext-minimized contracts only;
|
|
31
33
|
- billing records remain contract records until an external billing provider invoice flow is wired.
|
|
32
34
|
|
|
33
|
-
Individual surface builders emit `readiness.contract_ready: true`, `readiness.integration_kind: "contract_validator_only"`, and `readiness.hosted_cloud_sellable: false` because contract readiness is not provider wiring, legal approval, security review, or operator go-live approval. A customer lifecycle packet may only mark `hosted_cloud_sellable: true` when every lifecycle surface has a provided evidence ref and an explicit operator go-live approval ref is supplied; the packet remains evidence validation, not live hosted SaaS or provider wiring.
|
|
35
|
+
Individual surface builders emit `readiness.contract_ready: true`, `readiness.integration_kind: "contract_validator_only"`, and `readiness.hosted_cloud_sellable: false` because contract readiness is not provider wiring, legal approval, security review, or operator go-live approval. A customer lifecycle packet may only mark `hosted_cloud_sellable: true` when every lifecycle surface has a provided evidence ref and an explicit operator go-live approval ref is supplied; the packet remains evidence validation, not live hosted SaaS or provider wiring. An API key lifecycle packet keeps `customer_api_keys_live:false` by default and can become live-ready only when every required issue/rotate/revoke/audit evidence ref is provided and an operator approval ref is supplied. That live-ready state is still evidence validation only: it does not issue a customer API key, create a secret, call an auth provider, rotate/revoke a provider credential, or prove provider-side deletion.
|
|
34
36
|
|
|
35
37
|
## Externally blocked before hosted cloud can be sold
|
|
36
38
|
|
|
@@ -53,6 +55,14 @@ A `provided` operator evidence ref means the contract can point to external evid
|
|
|
53
55
|
|
|
54
56
|
The lifecycle packet records lifecycle phases, required surfaces, external blockers, missing evidence refs, no-secret/no-plaintext guarantees, and the sellability gate. Missing surfaces, blocked evidence refs, or absent operator go-live approval keep `hosted_cloud_sellable:false`.
|
|
55
57
|
|
|
58
|
+
## API key lifecycle packet
|
|
59
|
+
|
|
60
|
+
`npm run production:hosted-api-key -- --tenant <id> --subject <opaque-subject-ref> --operation <issue|rotate|revoke|audit> --out <file>` builds `enigma.hosted_cloud.api_key_lifecycle_packet.v1` readiness evidence for one customer API key lifecycle operation. Operators may pass repeatable `--evidence-ref <phase=status:ref>` values for the phases required by that operation, plus `--operator-approval-ref <ref>` when a reviewed operator approval exists.
|
|
61
|
+
|
|
62
|
+
The command writes public-safe validation evidence only. It must not receive or print raw API keys, bearer tokens, credentials, provider response bodies, plaintext prompts, raw memory, customer content, financial ROI claims, provider deletion claims, or model forgetting claims. It creates no hosted account, customer API key, provider secret, KMS key, rotation job, revocation job, audit export, invoice, support ticket, Cloudflare resource, provider resource, or deployment.
|
|
63
|
+
|
|
64
|
+
The packet records lifecycle events, required evidence refs, missing evidence refs, malformed operation blockers, the operator approval ref, readiness status, `customer_api_keys_live`, and no-secret/no-plaintext guarantees. Missing evidence refs, blocked evidence refs, malformed operation surfaces, or absent operator approval keep `customer_api_keys_live:false`.
|
|
65
|
+
|
|
56
66
|
## Non-claims
|
|
57
67
|
|
|
58
68
|
Hosted cloud collateral must not say or imply:
|
package/docs/sdk-api.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SDK and API guide
|
|
2
2
|
|
|
3
|
-
This guide covers the public package imports for `enigma-memory@0.1.
|
|
3
|
+
This guide covers the public package imports for `enigma-memory@0.1.11`. The SDK runs locally by default: vaults, passports, context packs, receipts, relay/gateway demo state, storage contracts, metering artifacts, settlement artifacts, and hosted-cloud contract packets are package-level developer surfaces. They are not evidence of hosted Enigma cloud, live customer API key issuance, provider-side deletion, provider model forgetting, token ROI, invoice savings, compliance certification, or benchmark leadership.
|
|
4
4
|
|
|
5
5
|
## Install and import style
|
|
6
6
|
|
|
@@ -157,6 +157,27 @@ const job = createPermissionlessMemoryJob({
|
|
|
157
157
|
});
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
### `enigma-memory/hosted-cloud`
|
|
161
|
+
|
|
162
|
+
The hosted-cloud module emits public-safe contract/readiness evidence only. Customer API key lifecycle packets model issue, rotate, revoke, and audit readiness with evidence refs, opaque subject refs, fingerprints, missing-evidence refs, readiness status, and operator approval refs. They never contain raw key material, provider payloads, plaintext prompts, raw memory, credentials, ROI claims, provider deletion claims, or model forgetting claims.
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import {
|
|
166
|
+
buildApiKeyLifecyclePacket,
|
|
167
|
+
validateApiKeyLifecyclePacket,
|
|
168
|
+
} from 'enigma-memory/hosted-cloud';
|
|
169
|
+
|
|
170
|
+
const packet = buildApiKeyLifecyclePacket({
|
|
171
|
+
tenant_id: 'tenant-alpha',
|
|
172
|
+
subject_ref: 'subject-ref-alpha',
|
|
173
|
+
environment: 'production',
|
|
174
|
+
operation: 'audit',
|
|
175
|
+
});
|
|
176
|
+
const valid = validateApiKeyLifecyclePacket(packet);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`customer_api_keys_live` stays `false` unless the issue/rotate/revoke/audit evidence refs are complete and an operator approval ref is supplied. Even then, the packet is readiness evidence validation only; it does not issue a key, create a secret, call KMS or auth providers, rotate or revoke a provider credential, or prove provider-side deletion.
|
|
180
|
+
|
|
160
181
|
## Verifying exported proof
|
|
161
182
|
|
|
162
183
|
Package consumers should verify exported bundles through the CLI or MCP verifier rather than importing source-only verifier internals. Treat full bundles as private unless you have reviewed and removed local import key material before sharing:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigma-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Provider-agnostic AI memory passport and offline-verifiable proof layer.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"scripts/collect-hosted-backend-live-evidence.mjs",
|
|
86
86
|
"scripts/build-hosted-probe-worker.mjs",
|
|
87
87
|
"scripts/build-hosted-customer-lifecycle.mjs",
|
|
88
|
+
"scripts/build-hosted-api-key-lifecycle.mjs",
|
|
88
89
|
"scripts/build-edge-backend-workers.mjs",
|
|
89
90
|
"scripts/validate-cloudflare-worker-inspect.mjs",
|
|
90
91
|
"scripts/validate-production-manifests.mjs",
|
|
@@ -177,6 +178,7 @@
|
|
|
177
178
|
"production:edge-live": "node scripts/collect-edge-backend-bootstrap-evidence.mjs",
|
|
178
179
|
"production:ref-draft": "node scripts/build-hosted-ref-draft.mjs",
|
|
179
180
|
"production:hosted-customer": "node scripts/build-hosted-customer-lifecycle.mjs",
|
|
181
|
+
"production:hosted-api-key": "node scripts/build-hosted-api-key-lifecycle.mjs",
|
|
180
182
|
"production:storage-bootstrap": "node scripts/cloudflare-storage-bootstrap.mjs",
|
|
181
183
|
"production:worker-inspect": "node scripts/validate-cloudflare-worker-inspect.mjs",
|
|
182
184
|
"production:cloudflare-credentials": "node scripts/validate-cloudflare-credentials.mjs",
|
|
@@ -9,6 +9,7 @@ export const HOSTED_CLOUD_DASHBOARD_SCHEMA = 'enigma.hosted_cloud.dashboard_summ
|
|
|
9
9
|
export const HOSTED_CLOUD_BACKUP_DRILL_SCHEMA = 'enigma.hosted_cloud.backup_drill.v1';
|
|
10
10
|
export const HOSTED_CLOUD_INCIDENT_SLA_SCHEMA = 'enigma.hosted_cloud.incident_sla_refs.v1';
|
|
11
11
|
export const HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA = 'enigma.hosted_cloud.customer_lifecycle_packet.v1';
|
|
12
|
+
export const HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA = 'enigma.hosted_cloud.api_key_lifecycle_packet.v1';
|
|
12
13
|
|
|
13
14
|
export const HOSTED_CLOUD_EXTERNAL_BLOCKERS = Object.freeze([
|
|
14
15
|
'auth_provider',
|
|
@@ -63,6 +64,23 @@ const CUSTOMER_LIFECYCLE_CONTRACT_PHASES = Object.freeze([
|
|
|
63
64
|
'incident_sla',
|
|
64
65
|
]);
|
|
65
66
|
const CUSTOMER_LIFECYCLE_REQUIRED_EVIDENCE_PHASES = Object.freeze(HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES.filter((phase) => phase !== 'operator_go_live'));
|
|
67
|
+
export const HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS = Object.freeze(['issue', 'rotate', 'revoke', 'audit']);
|
|
68
|
+
export const HOSTED_CLOUD_API_KEY_LIFECYCLE_PHASES = Object.freeze([
|
|
69
|
+
'current_key_metadata',
|
|
70
|
+
'next_key_metadata',
|
|
71
|
+
'revoked_key_metadata',
|
|
72
|
+
'issue_policy',
|
|
73
|
+
'rotation_policy',
|
|
74
|
+
'revocation_policy',
|
|
75
|
+
'audit_log',
|
|
76
|
+
]);
|
|
77
|
+
const API_KEY_LIFECYCLE_OPERATION_PHASES = Object.freeze({
|
|
78
|
+
issue: Object.freeze(['next_key_metadata', 'issue_policy', 'audit_log']),
|
|
79
|
+
rotate: Object.freeze(['current_key_metadata', 'next_key_metadata', 'rotation_policy', 'audit_log']),
|
|
80
|
+
revoke: Object.freeze(['current_key_metadata', 'revoked_key_metadata', 'revocation_policy', 'audit_log']),
|
|
81
|
+
audit: Object.freeze(['current_key_metadata', 'audit_log']),
|
|
82
|
+
});
|
|
83
|
+
const API_KEY_LIFECYCLE_KEY_SLOTS = Object.freeze(['current', 'next', 'revoked']);
|
|
66
84
|
const CUSTOMER_LIFECYCLE_BLOCKER_LABELS = Object.freeze({
|
|
67
85
|
account: 'Hosted account contract evidence is not provided.',
|
|
68
86
|
tenant: 'Hosted tenant contract evidence is not provided.',
|
|
@@ -78,8 +96,19 @@ const CUSTOMER_LIFECYCLE_BLOCKER_LABELS = Object.freeze({
|
|
|
78
96
|
security_review: 'Hosted security review evidence is not provided.',
|
|
79
97
|
operator_go_live: 'Explicit operator go-live approval is not provided.',
|
|
80
98
|
});
|
|
99
|
+
const API_KEY_LIFECYCLE_BLOCKER_LABELS = Object.freeze({
|
|
100
|
+
current_key_metadata: 'Current hosted API key metadata evidence is not provided.',
|
|
101
|
+
next_key_metadata: 'Next hosted API key metadata evidence is not provided.',
|
|
102
|
+
revoked_key_metadata: 'Revoked hosted API key metadata evidence is not provided.',
|
|
103
|
+
issue_policy: 'Hosted API key issue policy evidence is not provided.',
|
|
104
|
+
rotation_policy: 'Hosted API key rotation policy evidence is not provided.',
|
|
105
|
+
revocation_policy: 'Hosted API key revocation policy evidence is not provided.',
|
|
106
|
+
audit_log: 'Hosted API key lifecycle audit evidence is not provided.',
|
|
107
|
+
operator_approval: 'Explicit public-safe operator API key lifecycle approval is not provided.',
|
|
108
|
+
});
|
|
109
|
+
const API_KEY_SECRET_MATERIAL_KEY_RE = /(?:^|_)(?:raw_?api_?key|raw_?key|plaintext_?api_?key|plain_text_?api_?key|plaintext_?key|plain_text_?key|api_?key_?value|key_?value|key_?material|secret_?key|key_?secret|value)(?:$|_)/iu;
|
|
81
110
|
const FORBIDDEN_KEY_RE = /(?:^|_)(?:raw_?memory|plaintext|plain_text|prompt|prompts|completion|completions|message_body|transcript|conversation|provider_?response|response_?body|credential|credentials|secret|password|private_?key|bearer|access_token|refresh_token|token_value|api_key_value|api_secret|token_?roi|token_?profit|roi_claim|profit_claim|provider_?deletion|provider_?erasure|model_?forgetting|model_?erasure)(?:$|_)/iu;
|
|
82
|
-
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}
|
|
111
|
+
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;
|
|
83
112
|
const FORBIDDEN_CLAIM_RE = /(?:token\s+(?:roi|profit|return|investment|price)|(?: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;
|
|
84
113
|
|
|
85
114
|
function isPlainObject(value) {
|
|
@@ -880,7 +909,7 @@ function validateCustomerLifecycleEvidenceRefs(requiredEvidenceRefs) {
|
|
|
880
909
|
function validateCustomerLifecycleContracts(contracts) {
|
|
881
910
|
if (!isPlainObject(contracts)) throw new TypeError('contracts must be present');
|
|
882
911
|
for (const phase of CUSTOMER_LIFECYCLE_CONTRACT_PHASES) {
|
|
883
|
-
if (!Object.prototype.hasOwnProperty.call(contracts, phase)) throw new TypeError(`contracts.${phase} must be present`);
|
|
912
|
+
if (!Object.prototype.hasOwnProperty.call(contracts, phase)) throw new TypeError(`contracts.${phase} must be present; missing_surface_refs must match contracts`);
|
|
884
913
|
const contract = contracts[phase];
|
|
885
914
|
if (contract === null) continue;
|
|
886
915
|
if (!isPlainObject(contract)) throw new TypeError(`contracts.${phase} must be an object or null`);
|
|
@@ -982,3 +1011,392 @@ export function validateCustomerLifecyclePacket(packet) {
|
|
|
982
1011
|
validateCustomerLifecycleSafetyGuarantees(packet.guarantees ?? packet.public_safety_guarantees);
|
|
983
1012
|
return true;
|
|
984
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
function assertNoApiKeySecretMaterial(value, path = 'api_key_lifecycle') {
|
|
1016
|
+
if (Array.isArray(value)) {
|
|
1017
|
+
value.forEach((item, index) => assertNoApiKeySecretMaterial(item, `${path}[${index}]`));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (!isPlainObject(value)) return;
|
|
1021
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1022
|
+
const allowedBoundaryKey = key === 'key_material_boundary' || key === 'key_material_in_contract' || key === 'api_key_material_absent';
|
|
1023
|
+
const keyMetadataContext = /(?:^|\.)(?:current|next|revoked|current_key|currentKey|current_api_key|currentApiKey|next_key|nextKey|next_api_key|nextApiKey|revoked_key|revokedKey|revoked_api_key|revokedApiKey|key_contracts\.(?:current|next|revoked)|keyContracts\.(?:current|next|revoked)|key_metadata_contracts\.(?:current|next|revoked)|keyMetadataContracts\.(?:current|next|revoked)|contracts\.(?:current|next|revoked))$/u.test(path);
|
|
1024
|
+
const bareSecretKey = key === 'api_key' || key === 'apiKey' || (keyMetadataContext && key === 'key');
|
|
1025
|
+
if (!allowedBoundaryKey && (bareSecretKey || API_KEY_SECRET_MATERIAL_KEY_RE.test(key))) {
|
|
1026
|
+
throw new TypeError(`${path}.${key} is not allowed in API key lifecycle packets`);
|
|
1027
|
+
}
|
|
1028
|
+
assertNoApiKeySecretMaterial(child, `${path}.${key}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function apiKeyLifecycleOperation(input) {
|
|
1033
|
+
const operation = requiredString(input.operation, 'operation');
|
|
1034
|
+
if (!HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS.includes(operation)) {
|
|
1035
|
+
throw new TypeError(`operation must be one of ${HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS.join(', ')}`);
|
|
1036
|
+
}
|
|
1037
|
+
return operation;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function apiKeyLifecycleRequiredPhases(operation) {
|
|
1041
|
+
return API_KEY_LIFECYCLE_OPERATION_PHASES[operation];
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function apiKeyLifecycleSlotAliases(slot) {
|
|
1045
|
+
switch (slot) {
|
|
1046
|
+
case 'current':
|
|
1047
|
+
return ['current', 'current_key', 'currentKey', 'current_api_key', 'currentApiKey', 'current_key_contract', 'currentKeyContract', 'current_api_key_contract', 'currentApiKeyContract'];
|
|
1048
|
+
case 'next':
|
|
1049
|
+
return ['next', 'next_key', 'nextKey', 'next_api_key', 'nextApiKey', 'next_key_contract', 'nextKeyContract', 'next_api_key_contract', 'nextApiKeyContract'];
|
|
1050
|
+
case 'revoked':
|
|
1051
|
+
return ['revoked', 'revoked_key', 'revokedKey', 'revoked_api_key', 'revokedApiKey', 'revoked_key_contract', 'revokedKeyContract', 'revoked_api_key_contract', 'revokedApiKeyContract'];
|
|
1052
|
+
default:
|
|
1053
|
+
throw new TypeError(`unknown API key lifecycle slot: ${slot}`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function apiKeyLifecycleContractSource(input, slot) {
|
|
1058
|
+
const aliases = apiKeyLifecycleSlotAliases(slot);
|
|
1059
|
+
const contractCollections = [
|
|
1060
|
+
input.key_contracts,
|
|
1061
|
+
input.keyContracts,
|
|
1062
|
+
input.key_metadata_contracts,
|
|
1063
|
+
input.keyMetadataContracts,
|
|
1064
|
+
input.contracts,
|
|
1065
|
+
];
|
|
1066
|
+
for (const collection of contractCollections) {
|
|
1067
|
+
if (!isPlainObject(collection)) continue;
|
|
1068
|
+
for (const alias of aliases) {
|
|
1069
|
+
if (collection[alias] !== undefined) return collection[alias];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
for (const alias of aliases) {
|
|
1073
|
+
if (input[alias] !== undefined) return input[alias];
|
|
1074
|
+
}
|
|
1075
|
+
return undefined;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function validateApiKeyLifecycleContractTenantSubject(contract, tenantId, subjectRef, name) {
|
|
1079
|
+
if (contract.tenant_id !== tenantId) throw new TypeError(`${name}.tenant_id must match packet tenant_id`);
|
|
1080
|
+
if (contract.subject_ref !== subjectRef) throw new TypeError(`${name}.subject_ref must match packet subject_ref`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function buildOrValidateApiKeyLifecycleContract(slot, source, tenantId, subjectRef, generatedAt) {
|
|
1084
|
+
if (source === undefined || source === null) return null;
|
|
1085
|
+
if (!isPlainObject(source)) throw new TypeError(`key_contracts.${slot} must be an object`);
|
|
1086
|
+
const name = `key_contracts.${slot}`;
|
|
1087
|
+
if (source.schema === HOSTED_CLOUD_API_KEY_SCHEMA) {
|
|
1088
|
+
validateApiKeyContract(source);
|
|
1089
|
+
validateApiKeyLifecycleContractTenantSubject(source, tenantId, subjectRef, name);
|
|
1090
|
+
return source;
|
|
1091
|
+
}
|
|
1092
|
+
const contract = buildApiKeyContract({
|
|
1093
|
+
...source,
|
|
1094
|
+
tenant_id: source.tenant_id ?? source.tenantId ?? tenantId,
|
|
1095
|
+
subject_ref: source.subject_ref ?? source.subjectRef ?? subjectRef,
|
|
1096
|
+
generated_at: source.generated_at ?? source.generatedAt ?? generatedAt,
|
|
1097
|
+
});
|
|
1098
|
+
validateApiKeyLifecycleContractTenantSubject(contract, tenantId, subjectRef, name);
|
|
1099
|
+
return contract;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function apiKeyLifecycleContractsFrom(input, tenantId, subjectRef, generatedAt) {
|
|
1103
|
+
return Object.fromEntries(API_KEY_LIFECYCLE_KEY_SLOTS.map((slot) => [
|
|
1104
|
+
slot,
|
|
1105
|
+
buildOrValidateApiKeyLifecycleContract(slot, apiKeyLifecycleContractSource(input, slot), tenantId, subjectRef, generatedAt),
|
|
1106
|
+
]));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function explicitApiKeyLifecycleEvidenceRefs(input) {
|
|
1110
|
+
const refs = input.required_evidence_refs ?? input.requiredEvidenceRefs ?? input.api_key_evidence_refs ?? input.apiKeyEvidenceRefs ?? input.lifecycle_evidence_refs ?? input.lifecycleEvidenceRefs ?? input.evidence_refs ?? input.evidenceRefs;
|
|
1111
|
+
if (refs === undefined || refs === null) return {};
|
|
1112
|
+
if (!isPlainObject(refs)) throw new TypeError('required_evidence_refs must be an object');
|
|
1113
|
+
return refs;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function apiKeyLifecycleEvidenceRefFor(phase, value) {
|
|
1117
|
+
const fallback = { ref: `blocked:${phase}`, status: BLOCKED, blocker: API_KEY_LIFECYCLE_BLOCKER_LABELS[phase] };
|
|
1118
|
+
if (value === undefined || value === null) return fallback;
|
|
1119
|
+
if (typeof value === 'string') return { ref: requirePublicSafeLifecycleRef(value, `required_evidence_refs.${phase}`), status: PROVIDED };
|
|
1120
|
+
if (!isPlainObject(value)) throw new TypeError(`required_evidence_refs.${phase} must be a string or object`);
|
|
1121
|
+
const status = stringOrDefault(value.status, PROVIDED);
|
|
1122
|
+
if (!EVIDENCE_STATUSES.has(status)) throw new TypeError(`required_evidence_refs.${phase}.status is invalid`);
|
|
1123
|
+
const ref = status === PROVIDED
|
|
1124
|
+
? requirePublicSafeLifecycleRef(value.ref, `required_evidence_refs.${phase}.ref`)
|
|
1125
|
+
: requiredString(value.ref, `required_evidence_refs.${phase}.ref`);
|
|
1126
|
+
assertNoForbiddenPayload(ref, `required_evidence_refs.${phase}.ref`);
|
|
1127
|
+
assertNoApiKeySecretMaterial(ref, `required_evidence_refs.${phase}.ref`);
|
|
1128
|
+
const evidence = { ref, status };
|
|
1129
|
+
const owner = optionalString(value.owner, `required_evidence_refs.${phase}.owner`);
|
|
1130
|
+
const blocker = optionalString(value.blocker, `required_evidence_refs.${phase}.blocker`);
|
|
1131
|
+
const contractSchema = optionalString(value.contract_schema, `required_evidence_refs.${phase}.contract_schema`);
|
|
1132
|
+
const contractId = optionalString(value.contract_id, `required_evidence_refs.${phase}.contract_id`);
|
|
1133
|
+
if (owner) evidence.owner = owner;
|
|
1134
|
+
if (contractSchema) evidence.contract_schema = contractSchema;
|
|
1135
|
+
if (contractId) evidence.contract_id = contractId;
|
|
1136
|
+
if (status === BLOCKED) evidence.blocker = blocker ?? API_KEY_LIFECYCLE_BLOCKER_LABELS[phase];
|
|
1137
|
+
return evidence;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function apiKeyLifecycleSlotForPhase(phase) {
|
|
1141
|
+
switch (phase) {
|
|
1142
|
+
case 'current_key_metadata':
|
|
1143
|
+
return 'current';
|
|
1144
|
+
case 'next_key_metadata':
|
|
1145
|
+
return 'next';
|
|
1146
|
+
case 'revoked_key_metadata':
|
|
1147
|
+
return 'revoked';
|
|
1148
|
+
default:
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function apiKeyLifecycleContractEvidenceRef(phase, contract) {
|
|
1154
|
+
if (!contract) return apiKeyLifecycleEvidenceRefFor(phase, undefined);
|
|
1155
|
+
return {
|
|
1156
|
+
ref: requiredString(contract.contract_hash, `key_contracts.${apiKeyLifecycleSlotForPhase(phase)}.contract_hash`),
|
|
1157
|
+
status: PROVIDED,
|
|
1158
|
+
contract_schema: requiredString(contract.schema, `key_contracts.${apiKeyLifecycleSlotForPhase(phase)}.schema`),
|
|
1159
|
+
contract_id: requiredString(contract.api_key_id, `key_contracts.${apiKeyLifecycleSlotForPhase(phase)}.api_key_id`),
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function apiKeyLifecycleEvidenceRefsFrom(input, operation, keyContracts) {
|
|
1164
|
+
const explicitRefs = explicitApiKeyLifecycleEvidenceRefs(input);
|
|
1165
|
+
const requiredPhases = apiKeyLifecycleRequiredPhases(operation);
|
|
1166
|
+
const evidenceRefs = {};
|
|
1167
|
+
for (const phase of requiredPhases) {
|
|
1168
|
+
const slot = apiKeyLifecycleSlotForPhase(phase);
|
|
1169
|
+
evidenceRefs[phase] = explicitRefs[phase] === undefined && slot !== null
|
|
1170
|
+
? apiKeyLifecycleContractEvidenceRef(phase, keyContracts[slot])
|
|
1171
|
+
: apiKeyLifecycleEvidenceRefFor(phase, explicitRefs[phase]);
|
|
1172
|
+
}
|
|
1173
|
+
return evidenceRefs;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function apiKeyLifecycleOperatorApprovalRefFromInput(input) {
|
|
1177
|
+
const value = input.operator_approval_ref ?? input.operatorApprovalRef ?? input.operator_go_live_ref ?? input.operatorGoLiveRef;
|
|
1178
|
+
if (value === undefined || value === null) return null;
|
|
1179
|
+
return requirePublicSafeLifecycleRef(value, 'operator_approval_ref');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function apiKeyLifecycleOperatorApprovalRefFromPacket(packet) {
|
|
1183
|
+
if (!Object.prototype.hasOwnProperty.call(packet, 'operator_approval_ref')) throw new TypeError('operator_approval_ref must be present');
|
|
1184
|
+
if (packet.operator_approval_ref === null) return null;
|
|
1185
|
+
return requirePublicSafeLifecycleRef(packet.operator_approval_ref, 'operator_approval_ref');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function missingApiKeyLifecycleEvidenceRefs(requiredEvidenceRefs, operation) {
|
|
1189
|
+
return apiKeyLifecycleRequiredPhases(operation)
|
|
1190
|
+
.filter((phase) => requiredEvidenceRefs[phase].status !== PROVIDED)
|
|
1191
|
+
.map((phase) => ({
|
|
1192
|
+
key: phase,
|
|
1193
|
+
ref: requiredEvidenceRefs[phase].ref,
|
|
1194
|
+
blocker: requiredEvidenceRefs[phase].blocker ?? API_KEY_LIFECYCLE_BLOCKER_LABELS[phase],
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function apiKeyLifecycleExternalBlockers(requiredEvidenceRefs, operation, operatorApprovalRef) {
|
|
1199
|
+
const blockers = missingApiKeyLifecycleEvidenceRefs(requiredEvidenceRefs, operation);
|
|
1200
|
+
if (operatorApprovalRef === null) {
|
|
1201
|
+
blockers.push({
|
|
1202
|
+
key: 'operator_approval',
|
|
1203
|
+
ref: 'blocked:operator_approval',
|
|
1204
|
+
blocker: API_KEY_LIFECYCLE_BLOCKER_LABELS.operator_approval,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return blockers;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function apiKeyLifecycleReadiness(operation, requiredEvidenceRefs, operatorApprovalRef) {
|
|
1211
|
+
const missingEvidenceRefs = missingApiKeyLifecycleEvidenceRefs(requiredEvidenceRefs, operation);
|
|
1212
|
+
const externalLifecycleBlockers = apiKeyLifecycleExternalBlockers(requiredEvidenceRefs, operation, operatorApprovalRef);
|
|
1213
|
+
const evidenceApproved = missingEvidenceRefs.length === 0 && operatorApprovalRef !== null;
|
|
1214
|
+
return {
|
|
1215
|
+
...HOSTED_CLOUD_CONTRACT_READY,
|
|
1216
|
+
operation,
|
|
1217
|
+
status: evidenceApproved ? 'operator_approved_api_key_lifecycle_evidence' : 'blocked_api_key_lifecycle_evidence_or_operator_approval',
|
|
1218
|
+
evidence_validation_only: true,
|
|
1219
|
+
no_provider_wiring: true,
|
|
1220
|
+
actual_key_issuance: false,
|
|
1221
|
+
lifecycle_evidence_complete: missingEvidenceRefs.length === 0,
|
|
1222
|
+
operator_approval_provided: operatorApprovalRef !== null,
|
|
1223
|
+
external_wiring_ready: externalLifecycleBlockers.length === 0,
|
|
1224
|
+
customer_api_keys_live: evidenceApproved,
|
|
1225
|
+
live_readiness_gate: evidenceApproved ? 'evidence_complete_operator_approved' : 'blocked_until_lifecycle_evidence_and_operator_approval',
|
|
1226
|
+
external_blockers: externalLifecycleBlockers,
|
|
1227
|
+
missing_evidence_refs: missingEvidenceRefs,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function apiKeyLifecycleKeyMetadataRefs(operation, requiredEvidenceRefs) {
|
|
1232
|
+
const requiredPhases = apiKeyLifecycleRequiredPhases(operation);
|
|
1233
|
+
return Object.fromEntries(API_KEY_LIFECYCLE_KEY_SLOTS.map((slot) => {
|
|
1234
|
+
const phase = `${slot}_key_metadata`;
|
|
1235
|
+
return [slot, requiredPhases.includes(phase) ? requiredEvidenceRefs[phase] : null];
|
|
1236
|
+
}));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function apiKeyLifecycleEvents(operation, requiredEvidenceRefs, keyContracts) {
|
|
1240
|
+
return apiKeyLifecycleRequiredPhases(operation).map((phase) => {
|
|
1241
|
+
const evidence = requiredEvidenceRefs[phase];
|
|
1242
|
+
const event = {
|
|
1243
|
+
operation,
|
|
1244
|
+
phase,
|
|
1245
|
+
evidence_ref: evidence.ref,
|
|
1246
|
+
evidence_status: evidence.status,
|
|
1247
|
+
ready: evidence.status === PROVIDED,
|
|
1248
|
+
};
|
|
1249
|
+
if (evidence.status === BLOCKED) event.blocker = evidence.blocker ?? API_KEY_LIFECYCLE_BLOCKER_LABELS[phase];
|
|
1250
|
+
const slot = apiKeyLifecycleSlotForPhase(phase);
|
|
1251
|
+
if (slot !== null && keyContracts[slot]) {
|
|
1252
|
+
event.key_slot = slot;
|
|
1253
|
+
event.contract_schema = keyContracts[slot].schema;
|
|
1254
|
+
event.contract_id = keyContracts[slot].api_key_id;
|
|
1255
|
+
event.contract_hash = keyContracts[slot].contract_hash;
|
|
1256
|
+
}
|
|
1257
|
+
return event;
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function apiKeyLifecycleSafetyGuarantees() {
|
|
1262
|
+
return {
|
|
1263
|
+
opaque_reference_only: true,
|
|
1264
|
+
customer_content_absent: true,
|
|
1265
|
+
sensitive_text_absent: true,
|
|
1266
|
+
auth_material_absent: true,
|
|
1267
|
+
api_key_material_absent: true,
|
|
1268
|
+
provider_payloads_absent: true,
|
|
1269
|
+
provider_wiring_absent: true,
|
|
1270
|
+
evidence_validation_only: true,
|
|
1271
|
+
actual_key_issuance_absent: true,
|
|
1272
|
+
financial_outcome_claim_absent: true,
|
|
1273
|
+
remote_erasure_claim_absent: true,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function validateApiKeyLifecycleSafetyGuarantees(guarantees) {
|
|
1278
|
+
if (!isPlainObject(guarantees)) throw new TypeError('public_safety_guarantees must be present');
|
|
1279
|
+
requiredTrue(guarantees.opaque_reference_only, 'public_safety_guarantees.opaque_reference_only');
|
|
1280
|
+
requiredTrue(guarantees.customer_content_absent, 'public_safety_guarantees.customer_content_absent');
|
|
1281
|
+
requiredTrue(guarantees.sensitive_text_absent, 'public_safety_guarantees.sensitive_text_absent');
|
|
1282
|
+
requiredTrue(guarantees.auth_material_absent, 'public_safety_guarantees.auth_material_absent');
|
|
1283
|
+
requiredTrue(guarantees.api_key_material_absent, 'public_safety_guarantees.api_key_material_absent');
|
|
1284
|
+
requiredTrue(guarantees.provider_payloads_absent, 'public_safety_guarantees.provider_payloads_absent');
|
|
1285
|
+
requiredTrue(guarantees.provider_wiring_absent, 'public_safety_guarantees.provider_wiring_absent');
|
|
1286
|
+
requiredTrue(guarantees.evidence_validation_only, 'public_safety_guarantees.evidence_validation_only');
|
|
1287
|
+
requiredTrue(guarantees.actual_key_issuance_absent, 'public_safety_guarantees.actual_key_issuance_absent');
|
|
1288
|
+
requiredTrue(guarantees.financial_outcome_claim_absent, 'public_safety_guarantees.financial_outcome_claim_absent');
|
|
1289
|
+
requiredTrue(guarantees.remote_erasure_claim_absent, 'public_safety_guarantees.remote_erasure_claim_absent');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function validateApiKeyLifecycleContracts(keyContracts, tenantId, subjectRef) {
|
|
1293
|
+
if (!isPlainObject(keyContracts)) throw new TypeError('key_contracts must be present');
|
|
1294
|
+
for (const slot of API_KEY_LIFECYCLE_KEY_SLOTS) {
|
|
1295
|
+
if (!Object.prototype.hasOwnProperty.call(keyContracts, slot)) throw new TypeError(`key_contracts.${slot} must be present`);
|
|
1296
|
+
const contract = keyContracts[slot];
|
|
1297
|
+
if (contract === null) continue;
|
|
1298
|
+
if (!isPlainObject(contract)) throw new TypeError(`key_contracts.${slot} must be an object or null`);
|
|
1299
|
+
validateApiKeyContract(contract);
|
|
1300
|
+
validateApiKeyLifecycleContractTenantSubject(contract, tenantId, subjectRef, `key_contracts.${slot}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function validateApiKeyLifecycleEvidenceRefs(requiredEvidenceRefs, operation) {
|
|
1305
|
+
if (!isPlainObject(requiredEvidenceRefs)) throw new TypeError('required_evidence_refs must be present');
|
|
1306
|
+
const expectedPhases = apiKeyLifecycleRequiredPhases(operation);
|
|
1307
|
+
for (const phase of Object.keys(requiredEvidenceRefs)) {
|
|
1308
|
+
if (!expectedPhases.includes(phase)) throw new TypeError(`required_evidence_refs.${phase} is not required for ${operation}`);
|
|
1309
|
+
}
|
|
1310
|
+
const normalized = {};
|
|
1311
|
+
for (const phase of expectedPhases) {
|
|
1312
|
+
if (!Object.prototype.hasOwnProperty.call(requiredEvidenceRefs, phase)) throw new TypeError(`required_evidence_refs.${phase} must be present`);
|
|
1313
|
+
normalized[phase] = apiKeyLifecycleEvidenceRefFor(phase, requiredEvidenceRefs[phase]);
|
|
1314
|
+
}
|
|
1315
|
+
return normalized;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function assertSameLifecycleObject(actual, expected, name) {
|
|
1319
|
+
if (!isPlainObject(actual)) throw new TypeError(`${name} must be an object`);
|
|
1320
|
+
if (JSON.stringify(canonicalize(actual)) !== JSON.stringify(canonicalize(expected))) throw new TypeError(`${name} must match API key lifecycle evidence`);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function validateApiKeyLifecycleReadiness(readiness, expected) {
|
|
1324
|
+
if (!isPlainObject(readiness)) throw new TypeError('readiness must be present');
|
|
1325
|
+
requiredTrue(readiness.contract_ready, 'readiness.contract_ready');
|
|
1326
|
+
if (readiness.integration_kind !== HOSTED_CLOUD_CONTRACT_READY.integration_kind) throw new TypeError('readiness.integration_kind must remain contract_validator_only');
|
|
1327
|
+
requiredTrue(readiness.no_external_provider_calls, 'readiness.no_external_provider_calls');
|
|
1328
|
+
if (readiness.operation !== expected.operation) throw new TypeError('readiness.operation must match operation');
|
|
1329
|
+
if (readiness.status !== expected.status) throw new TypeError('readiness.status must match API key lifecycle evidence and operator approval');
|
|
1330
|
+
requiredTrue(readiness.evidence_validation_only, 'readiness.evidence_validation_only');
|
|
1331
|
+
requiredTrue(readiness.no_provider_wiring, 'readiness.no_provider_wiring');
|
|
1332
|
+
requiredFalse(readiness.actual_key_issuance, 'readiness.actual_key_issuance');
|
|
1333
|
+
if (readiness.lifecycle_evidence_complete !== expected.lifecycle_evidence_complete) throw new TypeError('readiness.lifecycle_evidence_complete must match required_evidence_refs');
|
|
1334
|
+
if (readiness.operator_approval_provided !== expected.operator_approval_provided) throw new TypeError('readiness.operator_approval_provided must match operator_approval_ref');
|
|
1335
|
+
if (readiness.external_wiring_ready !== expected.external_wiring_ready) throw new TypeError('readiness.external_wiring_ready must match blockers');
|
|
1336
|
+
if (readiness.customer_api_keys_live !== expected.customer_api_keys_live) throw new TypeError('readiness.customer_api_keys_live must match lifecycle evidence and operator approval');
|
|
1337
|
+
if (readiness.live_readiness_gate !== expected.live_readiness_gate) throw new TypeError('readiness.live_readiness_gate must match lifecycle evidence and operator approval');
|
|
1338
|
+
assertSameLifecycleArray(readiness.external_blockers, expected.external_blockers, 'readiness.external_blockers');
|
|
1339
|
+
assertSameLifecycleArray(readiness.missing_evidence_refs, expected.missing_evidence_refs, 'readiness.missing_evidence_refs');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
export function buildApiKeyLifecyclePacket(input = {}) {
|
|
1343
|
+
if (!isPlainObject(input)) throw new TypeError('buildApiKeyLifecyclePacket requires an options object');
|
|
1344
|
+
assertNoForbiddenPayload(input, 'input');
|
|
1345
|
+
assertNoApiKeySecretMaterial(input, 'input');
|
|
1346
|
+
const tenantId = requiredString(input.tenant_id ?? input.tenantId, 'tenant_id');
|
|
1347
|
+
const subjectRef = requiredString(input.subject_ref ?? input.subjectRef, 'subject_ref');
|
|
1348
|
+
const generatedAt = isoTimestamp(input.generated_at ?? input.generatedAt, 'generated_at');
|
|
1349
|
+
const operation = apiKeyLifecycleOperation(input);
|
|
1350
|
+
const keyContracts = apiKeyLifecycleContractsFrom(input, tenantId, subjectRef, generatedAt);
|
|
1351
|
+
const requiredEvidenceRefs = apiKeyLifecycleEvidenceRefsFrom(input, operation, keyContracts);
|
|
1352
|
+
const operatorApprovalRef = apiKeyLifecycleOperatorApprovalRefFromInput(input);
|
|
1353
|
+
const readiness = apiKeyLifecycleReadiness(operation, requiredEvidenceRefs, operatorApprovalRef);
|
|
1354
|
+
const body = {
|
|
1355
|
+
schema: HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA,
|
|
1356
|
+
packet_id: stringOrDefault(input.packet_id ?? input.packetId, undefined),
|
|
1357
|
+
generated_at: generatedAt,
|
|
1358
|
+
tenant_id: tenantId,
|
|
1359
|
+
subject_ref: subjectRef,
|
|
1360
|
+
operation,
|
|
1361
|
+
key_contracts: keyContracts,
|
|
1362
|
+
key_metadata_refs: apiKeyLifecycleKeyMetadataRefs(operation, requiredEvidenceRefs),
|
|
1363
|
+
lifecycle_events: apiKeyLifecycleEvents(operation, requiredEvidenceRefs, keyContracts),
|
|
1364
|
+
required_evidence_refs: requiredEvidenceRefs,
|
|
1365
|
+
external_blockers: readiness.external_blockers,
|
|
1366
|
+
missing_evidence_refs: readiness.missing_evidence_refs,
|
|
1367
|
+
operator_approval_ref: operatorApprovalRef,
|
|
1368
|
+
readiness,
|
|
1369
|
+
customer_api_keys_live: readiness.customer_api_keys_live,
|
|
1370
|
+
guarantees: apiKeyLifecycleSafetyGuarantees(),
|
|
1371
|
+
public_safety_guarantees: apiKeyLifecycleSafetyGuarantees(),
|
|
1372
|
+
};
|
|
1373
|
+
const packet = withContractIdentity(body, 'hcaklp', 'packet_id');
|
|
1374
|
+
validateApiKeyLifecyclePacket(packet);
|
|
1375
|
+
return packet;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export function validateApiKeyLifecyclePacket(packet) {
|
|
1379
|
+
if (!isPlainObject(packet)) throw new TypeError('packet must be an object');
|
|
1380
|
+
assertNoForbiddenPayload(packet, 'packet');
|
|
1381
|
+
assertNoApiKeySecretMaterial(packet, 'packet');
|
|
1382
|
+
if (packet.schema !== HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA) throw new TypeError(`schema must be ${HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA}`);
|
|
1383
|
+
requiredString(packet.packet_id, 'packet_id');
|
|
1384
|
+
isoTimestamp(packet.generated_at, 'generated_at');
|
|
1385
|
+
requiredString(packet.contract_hash, 'contract_hash');
|
|
1386
|
+
const tenantId = requiredString(packet.tenant_id, 'tenant_id');
|
|
1387
|
+
const subjectRef = requiredString(packet.subject_ref, 'subject_ref');
|
|
1388
|
+
const operation = apiKeyLifecycleOperation(packet);
|
|
1389
|
+
validateApiKeyLifecycleContracts(packet.key_contracts, tenantId, subjectRef);
|
|
1390
|
+
const requiredEvidenceRefs = validateApiKeyLifecycleEvidenceRefs(packet.required_evidence_refs, operation);
|
|
1391
|
+
const operatorApprovalRef = apiKeyLifecycleOperatorApprovalRefFromPacket(packet);
|
|
1392
|
+
const expectedReadiness = apiKeyLifecycleReadiness(operation, requiredEvidenceRefs, operatorApprovalRef);
|
|
1393
|
+
assertSameLifecycleObject(packet.key_metadata_refs, apiKeyLifecycleKeyMetadataRefs(operation, requiredEvidenceRefs), 'key_metadata_refs');
|
|
1394
|
+
assertSameLifecycleArray(packet.lifecycle_events, apiKeyLifecycleEvents(operation, requiredEvidenceRefs, packet.key_contracts), 'lifecycle_events');
|
|
1395
|
+
assertSameLifecycleArray(packet.external_blockers, expectedReadiness.external_blockers, 'external_blockers');
|
|
1396
|
+
assertSameLifecycleArray(packet.missing_evidence_refs, expectedReadiness.missing_evidence_refs, 'missing_evidence_refs');
|
|
1397
|
+
validateApiKeyLifecycleReadiness(packet.readiness, expectedReadiness);
|
|
1398
|
+
if (packet.customer_api_keys_live !== expectedReadiness.customer_api_keys_live) throw new TypeError('customer_api_keys_live must match readiness.customer_api_keys_live');
|
|
1399
|
+
validateApiKeyLifecycleSafetyGuarantees(packet.guarantees ?? packet.public_safety_guarantees);
|
|
1400
|
+
validateApiKeyLifecycleSafetyGuarantees(packet.public_safety_guarantees ?? packet.guarantees);
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|