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.
- package/README.md +36 -17
- package/apps/cli/bin/enigma.mjs +3126 -2848
- package/deploy/docker-compose.local-production-simulation.yml +12 -10
- package/docs/benchmark-attestation-network.md +487 -487
- package/docs/benchmark-reproducibility.md +228 -227
- package/docs/demo-proof-network.md +275 -275
- package/docs/developer-ecosystem.md +223 -223
- package/docs/developer-proof-quickstart.md +325 -325
- package/docs/enigma-memory-ready-conformance.md +376 -376
- package/docs/hosted-cloud-product.md +10 -0
- package/docs/install-anywhere.md +34 -17
- package/docs/installers-and-desktop.md +9 -7
- package/docs/proof-network-build-notes.md +240 -240
- package/docs/proof-network.md +257 -257
- package/docs/sdk-api.md +324 -324
- package/docs/solana-devnet-acceptance.md +48 -0
- package/docs/solana-proof-rail.md +453 -453
- package/examples/ci/github-actions.yml +6 -8
- package/package.json +272 -265
- package/packages/mcp-server/src/index.js +1185 -1185
- package/packages/passport/src/index.js +9 -5
- package/scripts/build-benchmark-proof-release.mjs +391 -0
- package/scripts/build-goal-completion-audit.mjs +11 -5
- package/scripts/build-hosted-api-key-lifecycle.mjs +274 -274
- package/scripts/build-hosted-customer-lifecycle.mjs +456 -456
- package/scripts/build-installer-assets.mjs +389 -273
- package/scripts/build-production-handoff-packet.mjs +7 -6
- package/scripts/build-production-unblocker.mjs +409 -0
- package/scripts/build-proof-network-packet.mjs +213 -213
- package/scripts/release-audit.mjs +71 -2
- package/scripts/run-standard-memory-benchmarks.mjs +1070 -1070
- package/scripts/wait-for-backend-ready.mjs +4 -2
|
@@ -1,274 +1,274 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { writeFile } from 'node:fs/promises';
|
|
4
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS,
|
|
9
|
-
HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA,
|
|
10
|
-
HOSTED_CLOUD_API_KEY_LIFECYCLE_PHASES,
|
|
11
|
-
buildApiKeyLifecyclePacket,
|
|
12
|
-
validateApiKeyLifecyclePacket,
|
|
13
|
-
} from '../packages/hosted-cloud/src/index.js';
|
|
14
|
-
|
|
15
|
-
export const HOSTED_API_KEY_LIFECYCLE_PACKET_SCHEMA = HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA;
|
|
16
|
-
export const HOSTED_API_KEY_LIFECYCLE_RELEASE_TARGET = '0.1.
|
|
17
|
-
|
|
18
|
-
const PROVIDED = 'provided';
|
|
19
|
-
const BLOCKED = 'blocked_external_dependency';
|
|
20
|
-
const ALLOWED_OPERATIONS = new Set(HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS);
|
|
21
|
-
const ALLOWED_STATUSES = new Set([PROVIDED, BLOCKED, 'blocked_missing_evidence']);
|
|
22
|
-
const STATUS_ALIASES = Object.freeze({
|
|
23
|
-
blocked: BLOCKED,
|
|
24
|
-
missing: BLOCKED,
|
|
25
|
-
blocked_missing_evidence: BLOCKED,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const OPERATION_EVIDENCE_KEYS = Object.freeze({
|
|
29
|
-
issue: Object.freeze(['next_key_metadata', 'issue_policy', 'audit_log']),
|
|
30
|
-
rotate: Object.freeze(['current_key_metadata', 'next_key_metadata', 'rotation_policy', 'audit_log']),
|
|
31
|
-
revoke: Object.freeze(['current_key_metadata', 'revoked_key_metadata', 'revocation_policy', 'audit_log']),
|
|
32
|
-
audit: Object.freeze(['current_key_metadata', 'audit_log']),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const ALL_EVIDENCE_KEYS = Object.freeze(HOSTED_CLOUD_API_KEY_LIFECYCLE_PHASES);
|
|
36
|
-
const ALL_EVIDENCE_KEY_SET = new Set(ALL_EVIDENCE_KEYS);
|
|
37
|
-
|
|
38
|
-
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[\s_-]*memory|plaintext[\s_-]*prompts?|plain[\s_-]*text[\s_-]*prompts?|private[\s_-]*prompts?|provider[\s_-]*responses?|full[\s_-]*transcript|decrypted[\s_-]*memory|credentials?|secrets?|passwords?|private[\s_-]*keys?|api[\s_-]*key[\s_-]*(?:secret|material|value)|api[\s_-]*secrets?|access[\s_-]*tokens?|refresh[\s_-]*tokens?|token[\s_-]*values?|credential[\s_-]*material)\b)/iu;
|
|
39
|
-
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;
|
|
40
|
-
const ABSOLUTE_LOCAL_PATH_RE = /^(?:[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|tmp|var|etc|mnt|Volumes)\b)/u;
|
|
41
|
-
|
|
42
|
-
function assertPublicSafeString(value, label) {
|
|
43
|
-
if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} must be a non-empty public ref`);
|
|
44
|
-
const normalized = value.trim();
|
|
45
|
-
if (SECRET_VALUE_RE.test(normalized) || FORBIDDEN_CLAIM_RE.test(normalized) || ABSOLUTE_LOCAL_PATH_RE.test(normalized)) {
|
|
46
|
-
throw new Error(`${label} contains non-public hosted-cloud material`);
|
|
47
|
-
}
|
|
48
|
-
return normalized;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function assertRelativeOutFile(value) {
|
|
52
|
-
const out = assertPublicSafeString(value, 'output file');
|
|
53
|
-
if (isAbsolute(out) || ABSOLUTE_LOCAL_PATH_RE.test(out)) throw new Error('output file must be a relative path');
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function normalizeStatus(status) {
|
|
58
|
-
const normalized = STATUS_ALIASES[status] ?? status;
|
|
59
|
-
if (!ALLOWED_STATUSES.has(normalized)) throw new Error('evidence status must be provided, blocked_external_dependency, or blocked_missing_evidence');
|
|
60
|
-
return normalized;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function defaultEvidenceRef(key) {
|
|
64
|
-
return {
|
|
65
|
-
status: BLOCKED,
|
|
66
|
-
ref: `blocked:${key}`,
|
|
67
|
-
blocker: `Public-safe ${key.replaceAll('_', ' ')} evidence is not provided.`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function normalizeEvidenceRecord(key, record) {
|
|
72
|
-
const status = normalizeStatus(record.status ?? PROVIDED);
|
|
73
|
-
const ref = assertPublicSafeString(record.ref, 'evidence ref');
|
|
74
|
-
const output = { status, ref };
|
|
75
|
-
if (status !== PROVIDED) output.blocker = record.blocker ?? defaultEvidenceRef(key).blocker;
|
|
76
|
-
return output;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function parseEvidenceRef(value) {
|
|
80
|
-
const raw = assertPublicSafeString(value, 'evidence ref argument');
|
|
81
|
-
const equalsIndex = raw.indexOf('=');
|
|
82
|
-
if (equalsIndex <= 0 || equalsIndex === raw.length - 1) throw new Error('evidence refs must use key=status:ref');
|
|
83
|
-
const key = raw.slice(0, equalsIndex).trim();
|
|
84
|
-
if (!ALL_EVIDENCE_KEY_SET.has(key)) throw new Error(`evidence key must be one of: ${ALL_EVIDENCE_KEYS.join(', ')}`);
|
|
85
|
-
const body = raw.slice(equalsIndex + 1).trim();
|
|
86
|
-
const colonIndex = body.indexOf(':');
|
|
87
|
-
if (colonIndex > 0) {
|
|
88
|
-
const possibleStatus = body.slice(0, colonIndex);
|
|
89
|
-
if (ALLOWED_STATUSES.has(possibleStatus) || STATUS_ALIASES[possibleStatus]) {
|
|
90
|
-
return { key, ...normalizeEvidenceRecord(key, { status: possibleStatus, ref: body.slice(colonIndex + 1) }) };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { key, ...normalizeEvidenceRecord(key, { status: PROVIDED, ref: body }) };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function evidenceRefsFor(operation, records = []) {
|
|
97
|
-
const requiredKeys = OPERATION_EVIDENCE_KEYS[operation];
|
|
98
|
-
const refs = Object.fromEntries(requiredKeys.map((key) => [key, defaultEvidenceRef(key)]));
|
|
99
|
-
for (const rawRecord of records) {
|
|
100
|
-
const record = typeof rawRecord === 'string' ? parseEvidenceRef(rawRecord) : rawRecord;
|
|
101
|
-
if (!requiredKeys.includes(record.key)) throw new Error(`evidence key ${record.key} is not required for ${operation}`);
|
|
102
|
-
refs[record.key] = normalizeEvidenceRecord(record.key, record);
|
|
103
|
-
}
|
|
104
|
-
return refs;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function providedRef(evidenceRefs, key) {
|
|
108
|
-
const evidence = evidenceRefs[key];
|
|
109
|
-
return evidence?.status === PROVIDED ? evidence.ref : null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function fingerprintFromRef(ref, label) {
|
|
113
|
-
if (!ref) return `blocked:${label}`;
|
|
114
|
-
return `sha256:${createHash('sha256').update(`${label}\0${ref}`).digest('hex')}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function scopeList(scopes = []) {
|
|
118
|
-
if (!Array.isArray(scopes) || scopes.length === 0) return ['blocked:scope'];
|
|
119
|
-
return Object.freeze(scopes.map((scope, index) => assertPublicSafeString(scope, `scope ${index + 1}`)));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
function keyInputsFor({ operation, tenantId, subjectRef, scopes, generatedAt, evidenceRefs }) {
|
|
124
|
-
const base = { tenant_id: tenantId, subject_ref: subjectRef, scopes, issued_at: generatedAt };
|
|
125
|
-
const currentRef = providedRef(evidenceRefs, 'current_key_metadata');
|
|
126
|
-
const nextRef = providedRef(evidenceRefs, 'next_key_metadata');
|
|
127
|
-
const revokedRef = providedRef(evidenceRefs, 'revoked_key_metadata');
|
|
128
|
-
const issueRef = providedRef(evidenceRefs, 'issue_policy') ?? nextRef;
|
|
129
|
-
const rotationRef = providedRef(evidenceRefs, 'rotation_policy') ?? currentRef;
|
|
130
|
-
const revokeRef = providedRef(evidenceRefs, 'revocation_policy') ?? revokedRef;
|
|
131
|
-
const inputs = {};
|
|
132
|
-
|
|
133
|
-
if (operation === 'issue') {
|
|
134
|
-
if (nextRef) inputs.next_key = { ...base, key_fingerprint: fingerprintFromRef(nextRef, 'issue_next_key'), rotation_ref: issueRef ?? 'blocked:issue_policy' };
|
|
135
|
-
return inputs;
|
|
136
|
-
}
|
|
137
|
-
if (operation === 'rotate') {
|
|
138
|
-
if (currentRef) inputs.current_key = { ...base, key_fingerprint: fingerprintFromRef(currentRef, 'rotate_current_key'), rotation_ref: rotationRef ?? 'blocked:rotation_policy' };
|
|
139
|
-
if (nextRef) inputs.next_key = { ...base, key_fingerprint: fingerprintFromRef(nextRef, 'rotate_next_key'), rotation_ref: rotationRef ?? 'blocked:rotation_policy' };
|
|
140
|
-
return inputs;
|
|
141
|
-
}
|
|
142
|
-
if (operation === 'revoke') {
|
|
143
|
-
if (currentRef) inputs.current_key = { ...base, key_fingerprint: fingerprintFromRef(currentRef, 'revoke_current_key'), rotation_ref: revokeRef ?? 'blocked:revocation_policy' };
|
|
144
|
-
if (revokedRef) inputs.revoked_key = { ...base, key_fingerprint: fingerprintFromRef(revokedRef, 'revoke_key'), rotation_ref: revokeRef ?? 'blocked:revocation_policy' };
|
|
145
|
-
return inputs;
|
|
146
|
-
}
|
|
147
|
-
if (currentRef) inputs.current_key = { ...base, key_fingerprint: fingerprintFromRef(currentRef, 'audit_key'), rotation_ref: providedRef(evidenceRefs, 'audit_log') ?? 'blocked:audit_log' };
|
|
148
|
-
return inputs;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function scriptBoundary() {
|
|
152
|
-
return {
|
|
153
|
-
public_safe_packet_only: true,
|
|
154
|
-
evidence_validation_only: true,
|
|
155
|
-
provider_wiring_performed: false,
|
|
156
|
-
deploys_or_calls_external_providers: false,
|
|
157
|
-
sensitive_material_written: false,
|
|
158
|
-
customer_api_key_material_written: false,
|
|
159
|
-
customer_content_written: false,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function buildHostedApiKeyLifecyclePacket(options = {}) {
|
|
164
|
-
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
165
|
-
const tenantId = assertPublicSafeString(options.tenant ?? options.tenantId ?? 'blocked:tenant', 'tenant');
|
|
166
|
-
const subjectRef = assertPublicSafeString(options.subject ?? options.subjectRef ?? 'blocked:subject', 'subject');
|
|
167
|
-
const operation = options.operation ?? 'audit';
|
|
168
|
-
if (!ALLOWED_OPERATIONS.has(operation)) throw new Error('operation must be issue, rotate, revoke, or audit');
|
|
169
|
-
const scopes = scopeList(options.scopes ?? options.scope ?? []);
|
|
170
|
-
const evidenceRefs = evidenceRefsFor(operation, options.evidenceRefs ?? []);
|
|
171
|
-
const operatorApprovalRef = options.operatorApprovalRef === undefined
|
|
172
|
-
? null
|
|
173
|
-
: assertPublicSafeString(options.operatorApprovalRef, 'operator approval ref');
|
|
174
|
-
const packet = buildApiKeyLifecyclePacket({
|
|
175
|
-
tenant_id: tenantId,
|
|
176
|
-
subject_ref: subjectRef,
|
|
177
|
-
generated_at: generatedAt,
|
|
178
|
-
operation,
|
|
179
|
-
...keyInputsFor({ operation, tenantId, subjectRef, scopes, generatedAt, evidenceRefs }),
|
|
180
|
-
required_evidence_refs: evidenceRefs,
|
|
181
|
-
operator_approval_ref: operatorApprovalRef,
|
|
182
|
-
});
|
|
183
|
-
validateApiKeyLifecyclePacket(packet);
|
|
184
|
-
return {
|
|
185
|
-
...packet,
|
|
186
|
-
schema: HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA,
|
|
187
|
-
release_target: HOSTED_API_KEY_LIFECYCLE_RELEASE_TARGET,
|
|
188
|
-
tenant_id: tenantId,
|
|
189
|
-
subject_ref: subjectRef,
|
|
190
|
-
operation,
|
|
191
|
-
scopes,
|
|
192
|
-
boundary: scriptBoundary(),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function parseArgs(argv) {
|
|
197
|
-
const args = {
|
|
198
|
-
tenant: undefined,
|
|
199
|
-
subject: undefined,
|
|
200
|
-
operation: undefined,
|
|
201
|
-
scopes: [],
|
|
202
|
-
evidenceRefs: [],
|
|
203
|
-
operatorApprovalRef: undefined,
|
|
204
|
-
out: undefined,
|
|
205
|
-
help: false,
|
|
206
|
-
};
|
|
207
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
208
|
-
const arg = argv[index];
|
|
209
|
-
if (arg === '--help' || arg === '-h') {
|
|
210
|
-
args.help = true;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const readValue = (name) => {
|
|
214
|
-
index += 1;
|
|
215
|
-
if (index >= argv.length || argv[index].startsWith('--')) throw new Error(`${name} requires a value`);
|
|
216
|
-
return argv[index];
|
|
217
|
-
};
|
|
218
|
-
if (arg === '--tenant') args.tenant = assertPublicSafeString(readValue('--tenant'), 'tenant');
|
|
219
|
-
else if (arg === '--subject') args.subject = assertPublicSafeString(readValue('--subject'), 'subject');
|
|
220
|
-
else if (arg === '--operation') args.operation = assertPublicSafeString(readValue('--operation'), 'operation');
|
|
221
|
-
else if (arg === '--scope') args.scopes.push(assertPublicSafeString(readValue('--scope'), `scope ${args.scopes.length + 1}`));
|
|
222
|
-
else if (arg === '--evidence-ref') args.evidenceRefs.push(parseEvidenceRef(readValue('--evidence-ref')));
|
|
223
|
-
else if (arg === '--operator-approval-ref') args.operatorApprovalRef = assertPublicSafeString(readValue('--operator-approval-ref'), 'operator approval ref');
|
|
224
|
-
else if (arg === '--out') args.out = assertRelativeOutFile(readValue('--out'));
|
|
225
|
-
else throw new Error('unknown option; use --help');
|
|
226
|
-
}
|
|
227
|
-
return args;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function usage() {
|
|
231
|
-
return `Usage: node scripts/build-hosted-api-key-lifecycle.mjs [options]
|
|
232
|
-
|
|
233
|
-
Build a public-safe hosted API key lifecycle evidence packet. The script validates metadata, fingerprints, and opaque refs only; it does not issue keys, call providers, create accounts, revoke provider credentials, or write secret material.
|
|
234
|
-
|
|
235
|
-
Options:
|
|
236
|
-
--tenant <id> Tenant id. Defaults to blocked:tenant.
|
|
237
|
-
--subject <ref> Subject ref. Defaults to blocked:subject.
|
|
238
|
-
--operation <issue|rotate|revoke|audit>
|
|
239
|
-
Lifecycle operation. Defaults to audit.
|
|
240
|
-
--scope <scope> Repeatable public-safe scope. Defaults to blocked:scope.
|
|
241
|
-
--evidence-ref <key=status:ref> Repeatable evidence ref. key=<ref> implies provided.
|
|
242
|
-
Status: provided, blocked_external_dependency, blocked_missing_evidence.
|
|
243
|
-
Keys: ${ALL_EVIDENCE_KEYS.join(', ')}
|
|
244
|
-
--operator-approval-ref <ref> Operator approval evidence ref.
|
|
245
|
-
--out <file> Also write the packet JSON to a relative file path.
|
|
246
|
-
--help Show this help.
|
|
247
|
-
`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export async function main(argv = process.argv.slice(2)) {
|
|
251
|
-
const args = parseArgs(argv);
|
|
252
|
-
if (args.help) {
|
|
253
|
-
process.stdout.write(usage());
|
|
254
|
-
return 0;
|
|
255
|
-
}
|
|
256
|
-
const packet = buildHostedApiKeyLifecyclePacket(args);
|
|
257
|
-
const json = `${JSON.stringify(packet, null, 2)}\n`;
|
|
258
|
-
if (args.out) {
|
|
259
|
-
try {
|
|
260
|
-
await writeFile(args.out, json, 'utf8');
|
|
261
|
-
} catch {
|
|
262
|
-
throw new Error('failed to write API key lifecycle packet output');
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
process.stdout.write(json);
|
|
266
|
-
return 0;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
270
|
-
main().catch((error) => {
|
|
271
|
-
process.stderr.write(`Error: ${error?.message ?? 'failed to build API key lifecycle packet'}\n`);
|
|
272
|
-
process.exitCode = 1;
|
|
273
|
-
});
|
|
274
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS,
|
|
9
|
+
HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA,
|
|
10
|
+
HOSTED_CLOUD_API_KEY_LIFECYCLE_PHASES,
|
|
11
|
+
buildApiKeyLifecyclePacket,
|
|
12
|
+
validateApiKeyLifecyclePacket,
|
|
13
|
+
} from '../packages/hosted-cloud/src/index.js';
|
|
14
|
+
|
|
15
|
+
export const HOSTED_API_KEY_LIFECYCLE_PACKET_SCHEMA = HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA;
|
|
16
|
+
export const HOSTED_API_KEY_LIFECYCLE_RELEASE_TARGET = '0.1.15';
|
|
17
|
+
|
|
18
|
+
const PROVIDED = 'provided';
|
|
19
|
+
const BLOCKED = 'blocked_external_dependency';
|
|
20
|
+
const ALLOWED_OPERATIONS = new Set(HOSTED_CLOUD_API_KEY_LIFECYCLE_OPERATIONS);
|
|
21
|
+
const ALLOWED_STATUSES = new Set([PROVIDED, BLOCKED, 'blocked_missing_evidence']);
|
|
22
|
+
const STATUS_ALIASES = Object.freeze({
|
|
23
|
+
blocked: BLOCKED,
|
|
24
|
+
missing: BLOCKED,
|
|
25
|
+
blocked_missing_evidence: BLOCKED,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const OPERATION_EVIDENCE_KEYS = Object.freeze({
|
|
29
|
+
issue: Object.freeze(['next_key_metadata', 'issue_policy', 'audit_log']),
|
|
30
|
+
rotate: Object.freeze(['current_key_metadata', 'next_key_metadata', 'rotation_policy', 'audit_log']),
|
|
31
|
+
revoke: Object.freeze(['current_key_metadata', 'revoked_key_metadata', 'revocation_policy', 'audit_log']),
|
|
32
|
+
audit: Object.freeze(['current_key_metadata', 'audit_log']),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const ALL_EVIDENCE_KEYS = Object.freeze(HOSTED_CLOUD_API_KEY_LIFECYCLE_PHASES);
|
|
36
|
+
const ALL_EVIDENCE_KEY_SET = new Set(ALL_EVIDENCE_KEYS);
|
|
37
|
+
|
|
38
|
+
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[\s_-]*memory|plaintext[\s_-]*prompts?|plain[\s_-]*text[\s_-]*prompts?|private[\s_-]*prompts?|provider[\s_-]*responses?|full[\s_-]*transcript|decrypted[\s_-]*memory|credentials?|secrets?|passwords?|private[\s_-]*keys?|api[\s_-]*key[\s_-]*(?:secret|material|value)|api[\s_-]*secrets?|access[\s_-]*tokens?|refresh[\s_-]*tokens?|token[\s_-]*values?|credential[\s_-]*material)\b)/iu;
|
|
39
|
+
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;
|
|
40
|
+
const ABSOLUTE_LOCAL_PATH_RE = /^(?:[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|tmp|var|etc|mnt|Volumes)\b)/u;
|
|
41
|
+
|
|
42
|
+
function assertPublicSafeString(value, label) {
|
|
43
|
+
if (typeof value !== 'string' || value.trim() === '') throw new Error(`${label} must be a non-empty public ref`);
|
|
44
|
+
const normalized = value.trim();
|
|
45
|
+
if (SECRET_VALUE_RE.test(normalized) || FORBIDDEN_CLAIM_RE.test(normalized) || ABSOLUTE_LOCAL_PATH_RE.test(normalized)) {
|
|
46
|
+
throw new Error(`${label} contains non-public hosted-cloud material`);
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertRelativeOutFile(value) {
|
|
52
|
+
const out = assertPublicSafeString(value, 'output file');
|
|
53
|
+
if (isAbsolute(out) || ABSOLUTE_LOCAL_PATH_RE.test(out)) throw new Error('output file must be a relative path');
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeStatus(status) {
|
|
58
|
+
const normalized = STATUS_ALIASES[status] ?? status;
|
|
59
|
+
if (!ALLOWED_STATUSES.has(normalized)) throw new Error('evidence status must be provided, blocked_external_dependency, or blocked_missing_evidence');
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function defaultEvidenceRef(key) {
|
|
64
|
+
return {
|
|
65
|
+
status: BLOCKED,
|
|
66
|
+
ref: `blocked:${key}`,
|
|
67
|
+
blocker: `Public-safe ${key.replaceAll('_', ' ')} evidence is not provided.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeEvidenceRecord(key, record) {
|
|
72
|
+
const status = normalizeStatus(record.status ?? PROVIDED);
|
|
73
|
+
const ref = assertPublicSafeString(record.ref, 'evidence ref');
|
|
74
|
+
const output = { status, ref };
|
|
75
|
+
if (status !== PROVIDED) output.blocker = record.blocker ?? defaultEvidenceRef(key).blocker;
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseEvidenceRef(value) {
|
|
80
|
+
const raw = assertPublicSafeString(value, 'evidence ref argument');
|
|
81
|
+
const equalsIndex = raw.indexOf('=');
|
|
82
|
+
if (equalsIndex <= 0 || equalsIndex === raw.length - 1) throw new Error('evidence refs must use key=status:ref');
|
|
83
|
+
const key = raw.slice(0, equalsIndex).trim();
|
|
84
|
+
if (!ALL_EVIDENCE_KEY_SET.has(key)) throw new Error(`evidence key must be one of: ${ALL_EVIDENCE_KEYS.join(', ')}`);
|
|
85
|
+
const body = raw.slice(equalsIndex + 1).trim();
|
|
86
|
+
const colonIndex = body.indexOf(':');
|
|
87
|
+
if (colonIndex > 0) {
|
|
88
|
+
const possibleStatus = body.slice(0, colonIndex);
|
|
89
|
+
if (ALLOWED_STATUSES.has(possibleStatus) || STATUS_ALIASES[possibleStatus]) {
|
|
90
|
+
return { key, ...normalizeEvidenceRecord(key, { status: possibleStatus, ref: body.slice(colonIndex + 1) }) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { key, ...normalizeEvidenceRecord(key, { status: PROVIDED, ref: body }) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function evidenceRefsFor(operation, records = []) {
|
|
97
|
+
const requiredKeys = OPERATION_EVIDENCE_KEYS[operation];
|
|
98
|
+
const refs = Object.fromEntries(requiredKeys.map((key) => [key, defaultEvidenceRef(key)]));
|
|
99
|
+
for (const rawRecord of records) {
|
|
100
|
+
const record = typeof rawRecord === 'string' ? parseEvidenceRef(rawRecord) : rawRecord;
|
|
101
|
+
if (!requiredKeys.includes(record.key)) throw new Error(`evidence key ${record.key} is not required for ${operation}`);
|
|
102
|
+
refs[record.key] = normalizeEvidenceRecord(record.key, record);
|
|
103
|
+
}
|
|
104
|
+
return refs;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function providedRef(evidenceRefs, key) {
|
|
108
|
+
const evidence = evidenceRefs[key];
|
|
109
|
+
return evidence?.status === PROVIDED ? evidence.ref : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function fingerprintFromRef(ref, label) {
|
|
113
|
+
if (!ref) return `blocked:${label}`;
|
|
114
|
+
return `sha256:${createHash('sha256').update(`${label}\0${ref}`).digest('hex')}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scopeList(scopes = []) {
|
|
118
|
+
if (!Array.isArray(scopes) || scopes.length === 0) return ['blocked:scope'];
|
|
119
|
+
return Object.freeze(scopes.map((scope, index) => assertPublicSafeString(scope, `scope ${index + 1}`)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
function keyInputsFor({ operation, tenantId, subjectRef, scopes, generatedAt, evidenceRefs }) {
|
|
124
|
+
const base = { tenant_id: tenantId, subject_ref: subjectRef, scopes, issued_at: generatedAt };
|
|
125
|
+
const currentRef = providedRef(evidenceRefs, 'current_key_metadata');
|
|
126
|
+
const nextRef = providedRef(evidenceRefs, 'next_key_metadata');
|
|
127
|
+
const revokedRef = providedRef(evidenceRefs, 'revoked_key_metadata');
|
|
128
|
+
const issueRef = providedRef(evidenceRefs, 'issue_policy') ?? nextRef;
|
|
129
|
+
const rotationRef = providedRef(evidenceRefs, 'rotation_policy') ?? currentRef;
|
|
130
|
+
const revokeRef = providedRef(evidenceRefs, 'revocation_policy') ?? revokedRef;
|
|
131
|
+
const inputs = {};
|
|
132
|
+
|
|
133
|
+
if (operation === 'issue') {
|
|
134
|
+
if (nextRef) inputs.next_key = { ...base, key_fingerprint: fingerprintFromRef(nextRef, 'issue_next_key'), rotation_ref: issueRef ?? 'blocked:issue_policy' };
|
|
135
|
+
return inputs;
|
|
136
|
+
}
|
|
137
|
+
if (operation === 'rotate') {
|
|
138
|
+
if (currentRef) inputs.current_key = { ...base, key_fingerprint: fingerprintFromRef(currentRef, 'rotate_current_key'), rotation_ref: rotationRef ?? 'blocked:rotation_policy' };
|
|
139
|
+
if (nextRef) inputs.next_key = { ...base, key_fingerprint: fingerprintFromRef(nextRef, 'rotate_next_key'), rotation_ref: rotationRef ?? 'blocked:rotation_policy' };
|
|
140
|
+
return inputs;
|
|
141
|
+
}
|
|
142
|
+
if (operation === 'revoke') {
|
|
143
|
+
if (currentRef) inputs.current_key = { ...base, key_fingerprint: fingerprintFromRef(currentRef, 'revoke_current_key'), rotation_ref: revokeRef ?? 'blocked:revocation_policy' };
|
|
144
|
+
if (revokedRef) inputs.revoked_key = { ...base, key_fingerprint: fingerprintFromRef(revokedRef, 'revoke_key'), rotation_ref: revokeRef ?? 'blocked:revocation_policy' };
|
|
145
|
+
return inputs;
|
|
146
|
+
}
|
|
147
|
+
if (currentRef) inputs.current_key = { ...base, key_fingerprint: fingerprintFromRef(currentRef, 'audit_key'), rotation_ref: providedRef(evidenceRefs, 'audit_log') ?? 'blocked:audit_log' };
|
|
148
|
+
return inputs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scriptBoundary() {
|
|
152
|
+
return {
|
|
153
|
+
public_safe_packet_only: true,
|
|
154
|
+
evidence_validation_only: true,
|
|
155
|
+
provider_wiring_performed: false,
|
|
156
|
+
deploys_or_calls_external_providers: false,
|
|
157
|
+
sensitive_material_written: false,
|
|
158
|
+
customer_api_key_material_written: false,
|
|
159
|
+
customer_content_written: false,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function buildHostedApiKeyLifecyclePacket(options = {}) {
|
|
164
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
165
|
+
const tenantId = assertPublicSafeString(options.tenant ?? options.tenantId ?? 'blocked:tenant', 'tenant');
|
|
166
|
+
const subjectRef = assertPublicSafeString(options.subject ?? options.subjectRef ?? 'blocked:subject', 'subject');
|
|
167
|
+
const operation = options.operation ?? 'audit';
|
|
168
|
+
if (!ALLOWED_OPERATIONS.has(operation)) throw new Error('operation must be issue, rotate, revoke, or audit');
|
|
169
|
+
const scopes = scopeList(options.scopes ?? options.scope ?? []);
|
|
170
|
+
const evidenceRefs = evidenceRefsFor(operation, options.evidenceRefs ?? []);
|
|
171
|
+
const operatorApprovalRef = options.operatorApprovalRef === undefined
|
|
172
|
+
? null
|
|
173
|
+
: assertPublicSafeString(options.operatorApprovalRef, 'operator approval ref');
|
|
174
|
+
const packet = buildApiKeyLifecyclePacket({
|
|
175
|
+
tenant_id: tenantId,
|
|
176
|
+
subject_ref: subjectRef,
|
|
177
|
+
generated_at: generatedAt,
|
|
178
|
+
operation,
|
|
179
|
+
...keyInputsFor({ operation, tenantId, subjectRef, scopes, generatedAt, evidenceRefs }),
|
|
180
|
+
required_evidence_refs: evidenceRefs,
|
|
181
|
+
operator_approval_ref: operatorApprovalRef,
|
|
182
|
+
});
|
|
183
|
+
validateApiKeyLifecyclePacket(packet);
|
|
184
|
+
return {
|
|
185
|
+
...packet,
|
|
186
|
+
schema: HOSTED_CLOUD_API_KEY_LIFECYCLE_PACKET_SCHEMA,
|
|
187
|
+
release_target: HOSTED_API_KEY_LIFECYCLE_RELEASE_TARGET,
|
|
188
|
+
tenant_id: tenantId,
|
|
189
|
+
subject_ref: subjectRef,
|
|
190
|
+
operation,
|
|
191
|
+
scopes,
|
|
192
|
+
boundary: scriptBoundary(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function parseArgs(argv) {
|
|
197
|
+
const args = {
|
|
198
|
+
tenant: undefined,
|
|
199
|
+
subject: undefined,
|
|
200
|
+
operation: undefined,
|
|
201
|
+
scopes: [],
|
|
202
|
+
evidenceRefs: [],
|
|
203
|
+
operatorApprovalRef: undefined,
|
|
204
|
+
out: undefined,
|
|
205
|
+
help: false,
|
|
206
|
+
};
|
|
207
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
208
|
+
const arg = argv[index];
|
|
209
|
+
if (arg === '--help' || arg === '-h') {
|
|
210
|
+
args.help = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const readValue = (name) => {
|
|
214
|
+
index += 1;
|
|
215
|
+
if (index >= argv.length || argv[index].startsWith('--')) throw new Error(`${name} requires a value`);
|
|
216
|
+
return argv[index];
|
|
217
|
+
};
|
|
218
|
+
if (arg === '--tenant') args.tenant = assertPublicSafeString(readValue('--tenant'), 'tenant');
|
|
219
|
+
else if (arg === '--subject') args.subject = assertPublicSafeString(readValue('--subject'), 'subject');
|
|
220
|
+
else if (arg === '--operation') args.operation = assertPublicSafeString(readValue('--operation'), 'operation');
|
|
221
|
+
else if (arg === '--scope') args.scopes.push(assertPublicSafeString(readValue('--scope'), `scope ${args.scopes.length + 1}`));
|
|
222
|
+
else if (arg === '--evidence-ref') args.evidenceRefs.push(parseEvidenceRef(readValue('--evidence-ref')));
|
|
223
|
+
else if (arg === '--operator-approval-ref') args.operatorApprovalRef = assertPublicSafeString(readValue('--operator-approval-ref'), 'operator approval ref');
|
|
224
|
+
else if (arg === '--out') args.out = assertRelativeOutFile(readValue('--out'));
|
|
225
|
+
else throw new Error('unknown option; use --help');
|
|
226
|
+
}
|
|
227
|
+
return args;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function usage() {
|
|
231
|
+
return `Usage: node scripts/build-hosted-api-key-lifecycle.mjs [options]
|
|
232
|
+
|
|
233
|
+
Build a public-safe hosted API key lifecycle evidence packet. The script validates metadata, fingerprints, and opaque refs only; it does not issue keys, call providers, create accounts, revoke provider credentials, or write secret material.
|
|
234
|
+
|
|
235
|
+
Options:
|
|
236
|
+
--tenant <id> Tenant id. Defaults to blocked:tenant.
|
|
237
|
+
--subject <ref> Subject ref. Defaults to blocked:subject.
|
|
238
|
+
--operation <issue|rotate|revoke|audit>
|
|
239
|
+
Lifecycle operation. Defaults to audit.
|
|
240
|
+
--scope <scope> Repeatable public-safe scope. Defaults to blocked:scope.
|
|
241
|
+
--evidence-ref <key=status:ref> Repeatable evidence ref. key=<ref> implies provided.
|
|
242
|
+
Status: provided, blocked_external_dependency, blocked_missing_evidence.
|
|
243
|
+
Keys: ${ALL_EVIDENCE_KEYS.join(', ')}
|
|
244
|
+
--operator-approval-ref <ref> Operator approval evidence ref.
|
|
245
|
+
--out <file> Also write the packet JSON to a relative file path.
|
|
246
|
+
--help Show this help.
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
251
|
+
const args = parseArgs(argv);
|
|
252
|
+
if (args.help) {
|
|
253
|
+
process.stdout.write(usage());
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
const packet = buildHostedApiKeyLifecyclePacket(args);
|
|
257
|
+
const json = `${JSON.stringify(packet, null, 2)}\n`;
|
|
258
|
+
if (args.out) {
|
|
259
|
+
try {
|
|
260
|
+
await writeFile(args.out, json, 'utf8');
|
|
261
|
+
} catch {
|
|
262
|
+
throw new Error('failed to write API key lifecycle packet output');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
process.stdout.write(json);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
270
|
+
main().catch((error) => {
|
|
271
|
+
process.stderr.write(`Error: ${error?.message ?? 'failed to build API key lifecycle packet'}\n`);
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
});
|
|
274
|
+
}
|