enigma-memory 0.1.8 → 0.1.10
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/docs/developer-ecosystem.md +1 -1
- package/docs/hosted-cloud-product.md +21 -2
- package/docs/sdk-api.md +22 -1
- package/package.json +5 -1
- package/packages/hosted-cloud/src/index.js +865 -1
- 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 +456 -0
- package/scripts/build-installer-assets.mjs +1 -1
- package/scripts/run-standard-memory-benchmarks.mjs +1 -1
|
@@ -8,6 +8,8 @@ export const HOSTED_CLOUD_USAGE_BILLING_SCHEMA = 'enigma.hosted_cloud.usage_bill
|
|
|
8
8
|
export const HOSTED_CLOUD_DASHBOARD_SCHEMA = 'enigma.hosted_cloud.dashboard_summary.v1';
|
|
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
|
+
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';
|
|
11
13
|
|
|
12
14
|
export const HOSTED_CLOUD_EXTERNAL_BLOCKERS = Object.freeze([
|
|
13
15
|
'auth_provider',
|
|
@@ -35,8 +37,78 @@ const BLOCKER_LABELS = Object.freeze({
|
|
|
35
37
|
support_ownership: 'Support ownership is not assigned.',
|
|
36
38
|
external_security_review: 'External security review is not complete.',
|
|
37
39
|
});
|
|
40
|
+
|
|
41
|
+
export const HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES = Object.freeze([
|
|
42
|
+
'account',
|
|
43
|
+
'tenant',
|
|
44
|
+
'vault',
|
|
45
|
+
'api_key',
|
|
46
|
+
'billing',
|
|
47
|
+
'dashboard',
|
|
48
|
+
'backup',
|
|
49
|
+
'incident_sla',
|
|
50
|
+
'support',
|
|
51
|
+
'monitoring',
|
|
52
|
+
'legal',
|
|
53
|
+
'security_review',
|
|
54
|
+
'operator_go_live',
|
|
55
|
+
]);
|
|
56
|
+
const CUSTOMER_LIFECYCLE_CONTRACT_PHASES = Object.freeze([
|
|
57
|
+
'account',
|
|
58
|
+
'tenant',
|
|
59
|
+
'vault',
|
|
60
|
+
'api_key',
|
|
61
|
+
'billing',
|
|
62
|
+
'dashboard',
|
|
63
|
+
'backup',
|
|
64
|
+
'incident_sla',
|
|
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']);
|
|
84
|
+
const CUSTOMER_LIFECYCLE_BLOCKER_LABELS = Object.freeze({
|
|
85
|
+
account: 'Hosted account contract evidence is not provided.',
|
|
86
|
+
tenant: 'Hosted tenant contract evidence is not provided.',
|
|
87
|
+
vault: 'Hosted vault contract evidence is not provided.',
|
|
88
|
+
api_key: 'Hosted API key metadata evidence is not provided.',
|
|
89
|
+
billing: 'Hosted billing evidence is not provided.',
|
|
90
|
+
dashboard: 'Hosted dashboard evidence is not provided.',
|
|
91
|
+
backup: 'Hosted backup drill evidence is not provided.',
|
|
92
|
+
incident_sla: 'Hosted incident/SLA evidence is not provided.',
|
|
93
|
+
support: 'Hosted support ownership evidence is not provided.',
|
|
94
|
+
monitoring: 'Hosted monitoring evidence is not provided.',
|
|
95
|
+
legal: 'Hosted legal approval evidence is not provided.',
|
|
96
|
+
security_review: 'Hosted security review evidence is not provided.',
|
|
97
|
+
operator_go_live: 'Explicit operator go-live approval is not provided.',
|
|
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;
|
|
38
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;
|
|
39
|
-
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;
|
|
40
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;
|
|
41
113
|
|
|
42
114
|
function isPlainObject(value) {
|
|
@@ -536,3 +608,795 @@ export function validateIncidentSlaRefs(contract) {
|
|
|
536
608
|
requiredTrue(contract.incident_boundary.external_review_required, 'incident_boundary.external_review_required');
|
|
537
609
|
return true;
|
|
538
610
|
}
|
|
611
|
+
|
|
612
|
+
function lifecycleContractIdKey(phase) {
|
|
613
|
+
switch (phase) {
|
|
614
|
+
case 'account': return 'account_id';
|
|
615
|
+
case 'tenant': return 'tenant_id';
|
|
616
|
+
case 'vault': return 'vault_id';
|
|
617
|
+
case 'api_key': return 'api_key_id';
|
|
618
|
+
case 'billing': return 'billing_record_id';
|
|
619
|
+
case 'dashboard': return 'dashboard_id';
|
|
620
|
+
case 'backup': return 'backup_drill_id';
|
|
621
|
+
case 'incident_sla': return 'incident_sla_id';
|
|
622
|
+
default: throw new TypeError(`unknown lifecycle phase: ${phase}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function lifecycleContractSource(input, aliases) {
|
|
627
|
+
if (isPlainObject(input.contracts)) {
|
|
628
|
+
for (const alias of aliases) {
|
|
629
|
+
if (input.contracts[alias] !== undefined) return input.contracts[alias];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
for (const alias of aliases) {
|
|
633
|
+
if (input[alias] !== undefined) return input[alias];
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function buildOrValidateLifecycleContract(phase, source) {
|
|
639
|
+
if (source === undefined || source === null) return null;
|
|
640
|
+
if (!isPlainObject(source)) throw new TypeError(`contracts.${phase} must be an object`);
|
|
641
|
+
switch (phase) {
|
|
642
|
+
case 'account':
|
|
643
|
+
if (source.schema === HOSTED_CLOUD_USER_ACCOUNT_SCHEMA) {
|
|
644
|
+
validateUserAccountContract(source);
|
|
645
|
+
return source;
|
|
646
|
+
}
|
|
647
|
+
return buildUserAccountContract(source);
|
|
648
|
+
case 'tenant':
|
|
649
|
+
if (source.schema === HOSTED_CLOUD_TENANT_SCHEMA) {
|
|
650
|
+
validateTenantContract(source);
|
|
651
|
+
return source;
|
|
652
|
+
}
|
|
653
|
+
return buildTenantContract(source);
|
|
654
|
+
case 'vault':
|
|
655
|
+
if (source.schema === HOSTED_CLOUD_VAULT_SCHEMA) {
|
|
656
|
+
validateHostedVaultContract(source);
|
|
657
|
+
return source;
|
|
658
|
+
}
|
|
659
|
+
return buildHostedVaultContract(source);
|
|
660
|
+
case 'api_key':
|
|
661
|
+
if (source.schema === HOSTED_CLOUD_API_KEY_SCHEMA) {
|
|
662
|
+
validateApiKeyContract(source);
|
|
663
|
+
return source;
|
|
664
|
+
}
|
|
665
|
+
return buildApiKeyContract(source);
|
|
666
|
+
case 'billing':
|
|
667
|
+
if (source.schema === HOSTED_CLOUD_USAGE_BILLING_SCHEMA) {
|
|
668
|
+
validateUsageBillingRecord(source);
|
|
669
|
+
return source;
|
|
670
|
+
}
|
|
671
|
+
return buildUsageBillingRecord(source);
|
|
672
|
+
case 'dashboard':
|
|
673
|
+
if (source.schema === HOSTED_CLOUD_DASHBOARD_SCHEMA) {
|
|
674
|
+
validateDashboardSummary(source);
|
|
675
|
+
return source;
|
|
676
|
+
}
|
|
677
|
+
return buildDashboardSummary(source);
|
|
678
|
+
case 'backup':
|
|
679
|
+
if (source.schema === HOSTED_CLOUD_BACKUP_DRILL_SCHEMA) {
|
|
680
|
+
validateBackupDrillContract(source);
|
|
681
|
+
return source;
|
|
682
|
+
}
|
|
683
|
+
return buildBackupDrillContract(source);
|
|
684
|
+
case 'incident_sla':
|
|
685
|
+
if (source.schema === HOSTED_CLOUD_INCIDENT_SLA_SCHEMA) {
|
|
686
|
+
validateIncidentSlaRefs(source);
|
|
687
|
+
return source;
|
|
688
|
+
}
|
|
689
|
+
return buildIncidentSlaRefs(source);
|
|
690
|
+
default:
|
|
691
|
+
throw new TypeError(`unknown lifecycle phase: ${phase}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function lifecycleContractsFrom(input) {
|
|
696
|
+
return {
|
|
697
|
+
account: buildOrValidateLifecycleContract('account', lifecycleContractSource(input, ['account', 'user_account', 'userAccount', 'account_contract', 'accountContract', 'user_account_contract', 'userAccountContract'])),
|
|
698
|
+
tenant: buildOrValidateLifecycleContract('tenant', lifecycleContractSource(input, ['tenant', 'tenant_contract', 'tenantContract'])),
|
|
699
|
+
vault: buildOrValidateLifecycleContract('vault', lifecycleContractSource(input, ['vault', 'hosted_vault', 'hostedVault', 'vault_contract', 'vaultContract', 'hosted_vault_contract', 'hostedVaultContract'])),
|
|
700
|
+
api_key: buildOrValidateLifecycleContract('api_key', lifecycleContractSource(input, ['api_key', 'apiKey', 'api_key_contract', 'apiKeyContract'])),
|
|
701
|
+
billing: buildOrValidateLifecycleContract('billing', lifecycleContractSource(input, ['billing', 'usage_billing_record', 'usageBillingRecord', 'billing_record', 'billingRecord', 'billing_contract', 'billingContract'])),
|
|
702
|
+
dashboard: buildOrValidateLifecycleContract('dashboard', lifecycleContractSource(input, ['dashboard', 'dashboard_summary', 'dashboardSummary', 'dashboard_contract', 'dashboardContract'])),
|
|
703
|
+
backup: buildOrValidateLifecycleContract('backup', lifecycleContractSource(input, ['backup', 'backup_drill', 'backupDrill', 'backup_contract', 'backupContract', 'backup_drill_contract', 'backupDrillContract'])),
|
|
704
|
+
incident_sla: buildOrValidateLifecycleContract('incident_sla', lifecycleContractSource(input, ['incident_sla', 'incidentSla', 'incident_sla_refs', 'incidentSlaRefs', 'incident_sla_contract', 'incidentSlaContract'])),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function explicitLifecycleEvidenceRefs(input) {
|
|
709
|
+
const refs = input.required_evidence_refs ?? input.requiredEvidenceRefs ?? input.lifecycle_evidence_refs ?? input.lifecycleEvidenceRefs ?? input.evidence_refs ?? input.evidenceRefs ?? input.operator_evidence_refs ?? input.operatorEvidenceRefs;
|
|
710
|
+
if (refs === undefined || refs === null) return {};
|
|
711
|
+
if (!isPlainObject(refs)) throw new TypeError('required_evidence_refs must be an object');
|
|
712
|
+
return refs;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function requirePublicSafeLifecycleRef(value, name) {
|
|
716
|
+
const ref = requiredString(value, name);
|
|
717
|
+
assertNoForbiddenPayload(ref, name);
|
|
718
|
+
if (ref.startsWith('blocked:')) throw new TypeError(`${name} must be a public-safe evidence ref, not a blocker ref`);
|
|
719
|
+
return ref;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function lifecycleEvidenceRefFor(key, value) {
|
|
723
|
+
const fallback = { ref: `blocked:${key}`, status: BLOCKED, blocker: CUSTOMER_LIFECYCLE_BLOCKER_LABELS[key] };
|
|
724
|
+
if (value === undefined || value === null) return fallback;
|
|
725
|
+
if (typeof value === 'string') return { ref: requirePublicSafeLifecycleRef(value, `required_evidence_refs.${key}`), status: PROVIDED };
|
|
726
|
+
if (!isPlainObject(value)) throw new TypeError(`required_evidence_refs.${key} must be a string or object`);
|
|
727
|
+
const status = stringOrDefault(value.status, PROVIDED);
|
|
728
|
+
if (!EVIDENCE_STATUSES.has(status)) throw new TypeError(`required_evidence_refs.${key}.status is invalid`);
|
|
729
|
+
const ref = status === PROVIDED
|
|
730
|
+
? requirePublicSafeLifecycleRef(value.ref, `required_evidence_refs.${key}.ref`)
|
|
731
|
+
: requiredString(value.ref, `required_evidence_refs.${key}.ref`);
|
|
732
|
+
assertNoForbiddenPayload(ref, `required_evidence_refs.${key}.ref`);
|
|
733
|
+
const evidence = { ref, status };
|
|
734
|
+
const owner = optionalString(value.owner, `required_evidence_refs.${key}.owner`);
|
|
735
|
+
const blocker = optionalString(value.blocker, `required_evidence_refs.${key}.blocker`);
|
|
736
|
+
if (owner) evidence.owner = owner;
|
|
737
|
+
if (status === BLOCKED) evidence.blocker = blocker ?? CUSTOMER_LIFECYCLE_BLOCKER_LABELS[key];
|
|
738
|
+
return evidence;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function contractEvidenceRef(phase, contract) {
|
|
742
|
+
if (!contract) return lifecycleEvidenceRefFor(phase, undefined);
|
|
743
|
+
const idKey = lifecycleContractIdKey(phase);
|
|
744
|
+
return {
|
|
745
|
+
ref: requiredString(contract.contract_hash, `contracts.${phase}.contract_hash`),
|
|
746
|
+
status: PROVIDED,
|
|
747
|
+
contract_schema: requiredString(contract.schema, `contracts.${phase}.schema`),
|
|
748
|
+
contract_id: requiredString(contract[idKey], `contracts.${phase}.${idKey}`),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function lifecycleEvidenceRefsFrom(input, contracts) {
|
|
753
|
+
const explicitRefs = explicitLifecycleEvidenceRefs(input);
|
|
754
|
+
const evidenceRefs = {};
|
|
755
|
+
for (const phase of CUSTOMER_LIFECYCLE_REQUIRED_EVIDENCE_PHASES) {
|
|
756
|
+
evidenceRefs[phase] = explicitRefs[phase] === undefined && CUSTOMER_LIFECYCLE_CONTRACT_PHASES.includes(phase)
|
|
757
|
+
? contractEvidenceRef(phase, contracts[phase])
|
|
758
|
+
: lifecycleEvidenceRefFor(phase, explicitRefs[phase]);
|
|
759
|
+
}
|
|
760
|
+
return evidenceRefs;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function operatorGoLiveRefFromInput(input) {
|
|
764
|
+
const value = input.operator_go_live_ref ?? input.operatorGoLiveRef;
|
|
765
|
+
if (value === undefined || value === null) return null;
|
|
766
|
+
return requirePublicSafeLifecycleRef(value, 'operator_go_live_ref');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function operatorGoLiveRefFromPacket(packet) {
|
|
770
|
+
if (!Object.prototype.hasOwnProperty.call(packet, 'operator_go_live_ref')) throw new TypeError('operator_go_live_ref must be present');
|
|
771
|
+
if (packet.operator_go_live_ref === null) return null;
|
|
772
|
+
return requirePublicSafeLifecycleRef(packet.operator_go_live_ref, 'operator_go_live_ref');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function missingLifecycleEvidenceRefs(requiredEvidenceRefs) {
|
|
776
|
+
return CUSTOMER_LIFECYCLE_REQUIRED_EVIDENCE_PHASES
|
|
777
|
+
.filter((phase) => requiredEvidenceRefs[phase].status !== PROVIDED)
|
|
778
|
+
.map((phase) => ({
|
|
779
|
+
key: phase,
|
|
780
|
+
ref: requiredEvidenceRefs[phase].ref,
|
|
781
|
+
blocker: requiredEvidenceRefs[phase].blocker ?? CUSTOMER_LIFECYCLE_BLOCKER_LABELS[phase],
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function lifecycleContractExternalBlockers(contracts) {
|
|
786
|
+
if (!isPlainObject(contracts)) return [];
|
|
787
|
+
const blockers = [];
|
|
788
|
+
for (const phase of CUSTOMER_LIFECYCLE_CONTRACT_PHASES) {
|
|
789
|
+
const contract = contracts[phase];
|
|
790
|
+
if (!isPlainObject(contract) || !Array.isArray(contract.readiness?.external_blockers)) continue;
|
|
791
|
+
for (const blocker of contract.readiness.external_blockers) {
|
|
792
|
+
blockers.push({
|
|
793
|
+
key: `${phase}.${requiredString(blocker.key, `contracts.${phase}.readiness.external_blockers.key`)}`,
|
|
794
|
+
ref: requiredString(blocker.ref, `contracts.${phase}.readiness.external_blockers.ref`),
|
|
795
|
+
blocker: stringOrDefault(blocker.blocker, CUSTOMER_LIFECYCLE_BLOCKER_LABELS[phase]),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return blockers;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function customerLifecycleExternalBlockers(requiredEvidenceRefs, operatorGoLiveRef, contracts = null) {
|
|
803
|
+
const blockers = missingLifecycleEvidenceRefs(requiredEvidenceRefs).concat(lifecycleContractExternalBlockers(contracts));
|
|
804
|
+
if (operatorGoLiveRef === null) {
|
|
805
|
+
blockers.push({
|
|
806
|
+
key: 'operator_go_live',
|
|
807
|
+
ref: 'blocked:operator_go_live',
|
|
808
|
+
blocker: CUSTOMER_LIFECYCLE_BLOCKER_LABELS.operator_go_live,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return blockers;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function missingLifecycleSurfaceRefs(contracts) {
|
|
815
|
+
return CUSTOMER_LIFECYCLE_CONTRACT_PHASES
|
|
816
|
+
.filter((phase) => contracts[phase] === null)
|
|
817
|
+
.map((phase) => ({
|
|
818
|
+
key: phase,
|
|
819
|
+
ref: `blocked:${phase}`,
|
|
820
|
+
blocker: CUSTOMER_LIFECYCLE_BLOCKER_LABELS[phase],
|
|
821
|
+
}));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function customerLifecycleReadiness(requiredEvidenceRefs, operatorGoLiveRef, contracts = null) {
|
|
825
|
+
const missingEvidenceRefs = missingLifecycleEvidenceRefs(requiredEvidenceRefs);
|
|
826
|
+
const contractBlockers = lifecycleContractExternalBlockers(contracts);
|
|
827
|
+
const externalLifecycleBlockers = customerLifecycleExternalBlockers(requiredEvidenceRefs, operatorGoLiveRef, contracts);
|
|
828
|
+
const sellable = missingEvidenceRefs.length === 0 && contractBlockers.length === 0 && operatorGoLiveRef !== null;
|
|
829
|
+
return {
|
|
830
|
+
...HOSTED_CLOUD_CONTRACT_READY,
|
|
831
|
+
status: sellable ? 'operator_approved_evidence_packet' : 'blocked_lifecycle_evidence_or_operator_go_live',
|
|
832
|
+
evidence_validation_only: true,
|
|
833
|
+
lifecycle_evidence_complete: missingEvidenceRefs.length === 0,
|
|
834
|
+
operator_go_live_approved: operatorGoLiveRef !== null,
|
|
835
|
+
external_wiring_ready: externalLifecycleBlockers.length === 0,
|
|
836
|
+
hosted_cloud_sellable: sellable,
|
|
837
|
+
selling_gate: sellable ? 'evidence_complete_operator_go_live_approved' : 'blocked_until_lifecycle_evidence_and_operator_go_live',
|
|
838
|
+
external_blockers: externalLifecycleBlockers,
|
|
839
|
+
missing_evidence_refs: missingEvidenceRefs,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function lifecyclePhaseRows(requiredEvidenceRefs, contracts, operatorGoLiveRef) {
|
|
844
|
+
return HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PHASES.map((phase) => {
|
|
845
|
+
if (phase === 'operator_go_live') {
|
|
846
|
+
return {
|
|
847
|
+
phase,
|
|
848
|
+
evidence_ref: operatorGoLiveRef ?? 'blocked:operator_go_live',
|
|
849
|
+
evidence_status: operatorGoLiveRef === null ? BLOCKED : PROVIDED,
|
|
850
|
+
ready: operatorGoLiveRef !== null,
|
|
851
|
+
blocker: operatorGoLiveRef === null ? CUSTOMER_LIFECYCLE_BLOCKER_LABELS.operator_go_live : undefined,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const evidence = requiredEvidenceRefs[phase];
|
|
855
|
+
const row = {
|
|
856
|
+
phase,
|
|
857
|
+
evidence_ref: evidence.ref,
|
|
858
|
+
evidence_status: evidence.status,
|
|
859
|
+
ready: evidence.status === PROVIDED,
|
|
860
|
+
};
|
|
861
|
+
if (evidence.status === BLOCKED) row.blocker = evidence.blocker ?? CUSTOMER_LIFECYCLE_BLOCKER_LABELS[phase];
|
|
862
|
+
if (CUSTOMER_LIFECYCLE_CONTRACT_PHASES.includes(phase) && contracts[phase]) {
|
|
863
|
+
row.contract_schema = contracts[phase].schema;
|
|
864
|
+
row.contract_id = contracts[phase][lifecycleContractIdKey(phase)];
|
|
865
|
+
row.contract_hash = contracts[phase].contract_hash;
|
|
866
|
+
}
|
|
867
|
+
return row;
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function customerLifecycleSafetyGuarantees() {
|
|
872
|
+
return {
|
|
873
|
+
opaque_reference_only: true,
|
|
874
|
+
customer_content_absent: true,
|
|
875
|
+
sensitive_text_absent: true,
|
|
876
|
+
auth_material_absent: true,
|
|
877
|
+
provider_payloads_absent: true,
|
|
878
|
+
financial_outcome_claim_absent: true,
|
|
879
|
+
remote_erasure_claim_absent: true,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function validateCustomerLifecycleSafetyGuarantees(guarantees) {
|
|
884
|
+
if (!isPlainObject(guarantees)) throw new TypeError('public_safety_guarantees must be present');
|
|
885
|
+
requiredTrue(guarantees.opaque_reference_only, 'public_safety_guarantees.opaque_reference_only');
|
|
886
|
+
requiredTrue(guarantees.customer_content_absent, 'public_safety_guarantees.customer_content_absent');
|
|
887
|
+
requiredTrue(guarantees.sensitive_text_absent, 'public_safety_guarantees.sensitive_text_absent');
|
|
888
|
+
requiredTrue(guarantees.auth_material_absent, 'public_safety_guarantees.auth_material_absent');
|
|
889
|
+
requiredTrue(guarantees.provider_payloads_absent, 'public_safety_guarantees.provider_payloads_absent');
|
|
890
|
+
requiredTrue(guarantees.financial_outcome_claim_absent, 'public_safety_guarantees.financial_outcome_claim_absent');
|
|
891
|
+
requiredTrue(guarantees.remote_erasure_claim_absent, 'public_safety_guarantees.remote_erasure_claim_absent');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function assertSameLifecycleArray(actual, expected, name) {
|
|
895
|
+
if (!Array.isArray(actual)) throw new TypeError(`${name} must be an array`);
|
|
896
|
+
if (JSON.stringify(canonicalize(actual)) !== JSON.stringify(canonicalize(expected))) throw new TypeError(`${name} must match required_evidence_refs and operator_go_live_ref`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function validateCustomerLifecycleEvidenceRefs(requiredEvidenceRefs) {
|
|
900
|
+
if (!isPlainObject(requiredEvidenceRefs)) throw new TypeError('required_evidence_refs must be present');
|
|
901
|
+
for (const phase of CUSTOMER_LIFECYCLE_REQUIRED_EVIDENCE_PHASES) {
|
|
902
|
+
if (!Object.prototype.hasOwnProperty.call(requiredEvidenceRefs, phase)) throw new TypeError(`required_evidence_refs.${phase} must be present`);
|
|
903
|
+
const evidence = lifecycleEvidenceRefFor(phase, requiredEvidenceRefs[phase]);
|
|
904
|
+
requiredEvidenceRefs[phase] = evidence;
|
|
905
|
+
}
|
|
906
|
+
return requiredEvidenceRefs;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function validateCustomerLifecycleContracts(contracts) {
|
|
910
|
+
if (!isPlainObject(contracts)) throw new TypeError('contracts must be present');
|
|
911
|
+
for (const phase of CUSTOMER_LIFECYCLE_CONTRACT_PHASES) {
|
|
912
|
+
if (!Object.prototype.hasOwnProperty.call(contracts, phase)) throw new TypeError(`contracts.${phase} must be present; missing_surface_refs must match contracts`);
|
|
913
|
+
const contract = contracts[phase];
|
|
914
|
+
if (contract === null) continue;
|
|
915
|
+
if (!isPlainObject(contract)) throw new TypeError(`contracts.${phase} must be an object or null`);
|
|
916
|
+
switch (phase) {
|
|
917
|
+
case 'account':
|
|
918
|
+
validateUserAccountContract(contract);
|
|
919
|
+
break;
|
|
920
|
+
case 'tenant':
|
|
921
|
+
validateTenantContract(contract);
|
|
922
|
+
break;
|
|
923
|
+
case 'vault':
|
|
924
|
+
validateHostedVaultContract(contract);
|
|
925
|
+
break;
|
|
926
|
+
case 'api_key':
|
|
927
|
+
validateApiKeyContract(contract);
|
|
928
|
+
break;
|
|
929
|
+
case 'billing':
|
|
930
|
+
validateUsageBillingRecord(contract);
|
|
931
|
+
break;
|
|
932
|
+
case 'dashboard':
|
|
933
|
+
validateDashboardSummary(contract);
|
|
934
|
+
break;
|
|
935
|
+
case 'backup':
|
|
936
|
+
validateBackupDrillContract(contract);
|
|
937
|
+
break;
|
|
938
|
+
case 'incident_sla':
|
|
939
|
+
validateIncidentSlaRefs(contract);
|
|
940
|
+
break;
|
|
941
|
+
default:
|
|
942
|
+
throw new TypeError(`unknown lifecycle phase: ${phase}`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function validateCustomerLifecycleReadiness(readiness, expected) {
|
|
948
|
+
if (!isPlainObject(readiness)) throw new TypeError('readiness must be present');
|
|
949
|
+
requiredTrue(readiness.contract_ready, 'readiness.contract_ready');
|
|
950
|
+
if (readiness.integration_kind !== HOSTED_CLOUD_CONTRACT_READY.integration_kind) throw new TypeError('readiness.integration_kind must remain contract_validator_only');
|
|
951
|
+
requiredTrue(readiness.no_external_provider_calls, 'readiness.no_external_provider_calls');
|
|
952
|
+
requiredTrue(readiness.evidence_validation_only, 'readiness.evidence_validation_only');
|
|
953
|
+
if (readiness.status !== expected.status) throw new TypeError('readiness.status must match lifecycle evidence and operator go-live approval');
|
|
954
|
+
if (readiness.lifecycle_evidence_complete !== expected.lifecycle_evidence_complete) throw new TypeError('readiness.lifecycle_evidence_complete must match required_evidence_refs');
|
|
955
|
+
if (readiness.operator_go_live_approved !== expected.operator_go_live_approved) throw new TypeError('readiness.operator_go_live_approved must match operator_go_live_ref');
|
|
956
|
+
if (readiness.external_wiring_ready !== expected.external_wiring_ready) throw new TypeError('readiness.external_wiring_ready must match blockers');
|
|
957
|
+
if (readiness.hosted_cloud_sellable !== expected.hosted_cloud_sellable) throw new TypeError('readiness.hosted_cloud_sellable must match lifecycle evidence and operator go-live approval');
|
|
958
|
+
if (readiness.selling_gate !== expected.selling_gate) throw new TypeError('readiness.selling_gate must match lifecycle evidence and operator go-live approval');
|
|
959
|
+
assertSameLifecycleArray(readiness.external_blockers, expected.external_blockers, 'readiness.external_blockers');
|
|
960
|
+
assertSameLifecycleArray(readiness.missing_evidence_refs, expected.missing_evidence_refs, 'readiness.missing_evidence_refs');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export function buildCustomerLifecyclePacket(input = {}) {
|
|
964
|
+
if (!isPlainObject(input)) throw new TypeError('buildCustomerLifecyclePacket requires an options object');
|
|
965
|
+
assertNoForbiddenPayload(input, 'input');
|
|
966
|
+
const contracts = lifecycleContractsFrom(input);
|
|
967
|
+
const requiredEvidenceRefs = lifecycleEvidenceRefsFrom(input, contracts);
|
|
968
|
+
const operatorGoLiveRef = operatorGoLiveRefFromInput(input);
|
|
969
|
+
const missingSurfaceRefs = missingLifecycleSurfaceRefs(contracts);
|
|
970
|
+
const readiness = customerLifecycleReadiness(requiredEvidenceRefs, operatorGoLiveRef, contracts);
|
|
971
|
+
const body = {
|
|
972
|
+
schema: HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA,
|
|
973
|
+
packet_id: stringOrDefault(input.packet_id ?? input.packetId, undefined),
|
|
974
|
+
generated_at: isoTimestamp(input.generated_at ?? input.generatedAt, 'generated_at'),
|
|
975
|
+
contracts,
|
|
976
|
+
required_evidence_refs: requiredEvidenceRefs,
|
|
977
|
+
lifecycle_phases: lifecyclePhaseRows(requiredEvidenceRefs, contracts, operatorGoLiveRef),
|
|
978
|
+
missing_surface_refs: missingSurfaceRefs,
|
|
979
|
+
external_blockers: readiness.external_blockers,
|
|
980
|
+
missing_evidence_refs: readiness.missing_evidence_refs,
|
|
981
|
+
operator_go_live_ref: operatorGoLiveRef,
|
|
982
|
+
readiness,
|
|
983
|
+
hosted_cloud_sellable: readiness.hosted_cloud_sellable,
|
|
984
|
+
guarantees: customerLifecycleSafetyGuarantees(),
|
|
985
|
+
public_safety_guarantees: customerLifecycleSafetyGuarantees(),
|
|
986
|
+
};
|
|
987
|
+
const packet = withContractIdentity(body, 'hcclp', 'packet_id');
|
|
988
|
+
validateCustomerLifecyclePacket(packet);
|
|
989
|
+
return packet;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export function validateCustomerLifecyclePacket(packet) {
|
|
993
|
+
if (!isPlainObject(packet)) throw new TypeError('packet must be an object');
|
|
994
|
+
assertNoForbiddenPayload(packet, 'packet');
|
|
995
|
+
if (packet.schema !== HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA) throw new TypeError(`schema must be ${HOSTED_CLOUD_CUSTOMER_LIFECYCLE_PACKET_SCHEMA}`);
|
|
996
|
+
requiredString(packet.packet_id, 'packet_id');
|
|
997
|
+
isoTimestamp(packet.generated_at, 'generated_at');
|
|
998
|
+
requiredString(packet.contract_hash, 'contract_hash');
|
|
999
|
+
validateCustomerLifecycleContracts(packet.contracts);
|
|
1000
|
+
if (!isPlainObject(packet.required_evidence_refs)) throw new TypeError('required_evidence_refs must be present');
|
|
1001
|
+
const requiredEvidenceRefs = validateCustomerLifecycleEvidenceRefs({ ...packet.required_evidence_refs });
|
|
1002
|
+
const operatorGoLiveRef = operatorGoLiveRefFromPacket(packet);
|
|
1003
|
+
const expectedReadiness = customerLifecycleReadiness(requiredEvidenceRefs, operatorGoLiveRef, packet.contracts);
|
|
1004
|
+
const expectedPhases = lifecyclePhaseRows(requiredEvidenceRefs, packet.contracts, operatorGoLiveRef);
|
|
1005
|
+
assertSameLifecycleArray(packet.lifecycle_phases, expectedPhases, 'lifecycle_phases');
|
|
1006
|
+
assertSameLifecycleArray(packet.external_blockers, expectedReadiness.external_blockers, 'external_blockers');
|
|
1007
|
+
assertSameLifecycleArray(packet.missing_surface_refs, missingLifecycleSurfaceRefs(packet.contracts), 'missing_surface_refs');
|
|
1008
|
+
assertSameLifecycleArray(packet.missing_evidence_refs, expectedReadiness.missing_evidence_refs, 'missing_evidence_refs');
|
|
1009
|
+
validateCustomerLifecycleReadiness(packet.readiness, expectedReadiness);
|
|
1010
|
+
if (packet.hosted_cloud_sellable !== expectedReadiness.hosted_cloud_sellable) throw new TypeError('hosted_cloud_sellable must match readiness.hosted_cloud_sellable');
|
|
1011
|
+
validateCustomerLifecycleSafetyGuarantees(packet.guarantees ?? packet.public_safety_guarantees);
|
|
1012
|
+
return true;
|
|
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
|
+
}
|