@emilia-protocol/gate 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +64 -0
- package/action-refusal-statement.js +1 -0
- package/coverage-reconciliation-attestation.js +1 -0
- package/dist/action-refusal-statement.d.ts +109 -0
- package/dist/action-refusal-statement.d.ts.map +1 -0
- package/dist/action-refusal-statement.js +333 -0
- package/dist/action-refusal-statement.js.map +1 -0
- package/dist/coverage-reconciliation-attestation.d.ts +29 -0
- package/dist/coverage-reconciliation-attestation.d.ts.map +1 -0
- package/dist/coverage-reconciliation-attestation.js +111 -0
- package/dist/coverage-reconciliation-attestation.js.map +1 -0
- package/dist/gate-qualification-v2.d.ts +4 -0
- package/dist/gate-qualification-v2.d.ts.map +1 -1
- package/dist/gate-qualification-v2.js +39 -47
- package/dist/gate-qualification-v2.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/loss-allocation-schedule.d.ts +53 -0
- package/dist/loss-allocation-schedule.d.ts.map +1 -0
- package/dist/loss-allocation-schedule.js +351 -0
- package/dist/loss-allocation-schedule.js.map +1 -0
- package/dist/loss-experience-feed.d.ts +82 -0
- package/dist/loss-experience-feed.d.ts.map +1 -0
- package/dist/loss-experience-feed.js +314 -0
- package/dist/loss-experience-feed.js.map +1 -0
- package/dist/open-exposure-ledger-postgres.d.ts +40 -0
- package/dist/open-exposure-ledger-postgres.d.ts.map +1 -0
- package/dist/open-exposure-ledger-postgres.js +577 -0
- package/dist/open-exposure-ledger-postgres.js.map +1 -0
- package/dist/open-exposure-ledger.d.ts +243 -0
- package/dist/open-exposure-ledger.d.ts.map +1 -0
- package/dist/open-exposure-ledger.js +794 -0
- package/dist/open-exposure-ledger.js.map +1 -0
- package/dist/receipt-census.d.ts +21 -0
- package/dist/receipt-census.d.ts.map +1 -0
- package/dist/receipt-census.js +162 -0
- package/dist/receipt-census.js.map +1 -0
- package/dist/reliance-program.d.ts +88 -0
- package/dist/reliance-program.d.ts.map +1 -0
- package/dist/reliance-program.js +437 -0
- package/dist/reliance-program.js.map +1 -0
- package/dist/reliance-risk-crypto.d.ts +28 -0
- package/dist/reliance-risk-crypto.d.ts.map +1 -0
- package/dist/reliance-risk-crypto.js +110 -0
- package/dist/reliance-risk-crypto.js.map +1 -0
- package/dist/remedy-program.d.ts.map +1 -1
- package/dist/remedy-program.js +28 -18
- package/dist/remedy-program.js.map +1 -1
- package/loss-allocation-schedule.js +1 -0
- package/loss-experience-feed.js +1 -0
- package/open-exposure-ledger-postgres.js +1 -0
- package/open-exposure-ledger.js +1 -0
- package/package.json +42 -2
- package/receipt-census.js +1 -0
- package/reliance-program.js +3 -0
- package/src/action-refusal-statement.ts +396 -0
- package/src/coverage-reconciliation-attestation.ts +107 -0
- package/src/gate-qualification-v2.ts +56 -46
- package/src/index.ts +7 -0
- package/src/loss-allocation-schedule.ts +427 -0
- package/src/loss-experience-feed.ts +398 -0
- package/src/open-exposure-ledger-postgres.ts +701 -0
- package/src/open-exposure-ledger.ts +1213 -0
- package/src/receipt-census.ts +163 -0
- package/src/reliance-program.ts +499 -0
- package/src/reliance-risk-crypto.ts +114 -0
- package/src/remedy-program.ts +28 -17
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
4
|
+
|
|
5
|
+
export type RiskRecord = Record<string, any>;
|
|
6
|
+
export type TrustedRiskKeys = Record<string, { issuer_id: string; public_key: string }>;
|
|
7
|
+
|
|
8
|
+
export const RISK_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
9
|
+
export const RISK_CAID = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
10
|
+
export const RISK_ID = /^[A-Za-z0-9][A-Za-z0-9:_.@/+\-]{0,511}$/;
|
|
11
|
+
const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/;
|
|
12
|
+
|
|
13
|
+
export function riskRecord(value: unknown): value is RiskRecord {
|
|
14
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
15
|
+
const prototype = Object.getPrototypeOf(value);
|
|
16
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
17
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
18
|
+
if (typeof key !== 'string') return false;
|
|
19
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
20
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function riskExact(value: unknown, keys: readonly string[]): value is RiskRecord {
|
|
25
|
+
return riskRecord(value) && Reflect.ownKeys(value).length === keys.length
|
|
26
|
+
&& keys.every((key) => Object.hasOwn(value, key));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function riskIdentifier(value: unknown): value is string {
|
|
30
|
+
return typeof value === 'string' && RISK_ID.test(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function riskInstant(value: unknown): number {
|
|
34
|
+
if (typeof value !== 'string') return NaN;
|
|
35
|
+
const match = value.match(RFC3339);
|
|
36
|
+
if (!match) return NaN;
|
|
37
|
+
const parsed = Date.parse(value);
|
|
38
|
+
if (!Number.isFinite(parsed)) return NaN;
|
|
39
|
+
return new Date(parsed).toISOString().slice(0, 19) === value.slice(0, 19) ? parsed : NaN;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function riskDigest(value: unknown): string {
|
|
43
|
+
return `sha256:${hashCanonical(value)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function riskClone<T>(value: T): T {
|
|
47
|
+
return JSON.parse(canonicalize(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function riskFreeze<T>(value: T): T {
|
|
51
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
52
|
+
Object.freeze(value);
|
|
53
|
+
for (const child of Object.values(value as RiskRecord)) riskFreeze(child);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function signingBytes(version: string, body: RiskRecord): Buffer {
|
|
58
|
+
return Buffer.from(`${version}\0${canonicalize(body)}`, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function signRiskBody(
|
|
62
|
+
version: string,
|
|
63
|
+
bodyInput: RiskRecord,
|
|
64
|
+
signer: { issuer_id: string; key_id: string; private_key: crypto.KeyLike },
|
|
65
|
+
): RiskRecord {
|
|
66
|
+
const key = signer.private_key instanceof crypto.KeyObject
|
|
67
|
+
? signer.private_key : crypto.createPrivateKey(signer.private_key);
|
|
68
|
+
if (key.asymmetricKeyType !== 'ed25519') throw new TypeError('risk artifact signing key must be Ed25519');
|
|
69
|
+
if (!riskIdentifier(signer.issuer_id) || !riskIdentifier(signer.key_id)) throw new TypeError('risk artifact issuer is invalid');
|
|
70
|
+
const body = riskClone({ ...bodyInput, issuer: { id: signer.issuer_id, key_id: signer.key_id } });
|
|
71
|
+
const bodyDigest = riskDigest(body);
|
|
72
|
+
const proof = {
|
|
73
|
+
algorithm: 'Ed25519',
|
|
74
|
+
key_id: signer.key_id,
|
|
75
|
+
body_digest: bodyDigest,
|
|
76
|
+
signature_b64u: crypto.sign(null, signingBytes(version, body), key).toString('base64url'),
|
|
77
|
+
};
|
|
78
|
+
return riskFreeze({ ...body, proof });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function verifyRiskBody(
|
|
82
|
+
artifact: unknown,
|
|
83
|
+
version: string,
|
|
84
|
+
trustedKeys: TrustedRiskKeys | undefined,
|
|
85
|
+
): { valid: boolean; reason: string | null; body: RiskRecord | null; artifact_digest: string | null } {
|
|
86
|
+
const refuse = (reason: string) => ({ valid: false, reason, body: null, artifact_digest: null });
|
|
87
|
+
try {
|
|
88
|
+
if (!riskRecord(artifact) || !riskRecord(artifact.proof) || !riskRecord(artifact.issuer)) return refuse('artifact_shape_invalid');
|
|
89
|
+
const proof = artifact.proof;
|
|
90
|
+
const { proof: _proof, ...body } = artifact;
|
|
91
|
+
if (artifact['@version'] !== version || !riskExact(artifact.issuer, ['id', 'key_id'])
|
|
92
|
+
|| !riskIdentifier(artifact.issuer.id) || !riskIdentifier(artifact.issuer.key_id)
|
|
93
|
+
|| !riskExact(proof, ['algorithm', 'key_id', 'body_digest', 'signature_b64u'])
|
|
94
|
+
|| proof.algorithm !== 'Ed25519' || proof.key_id !== artifact.issuer.key_id
|
|
95
|
+
|| typeof proof.body_digest !== 'string' || !RISK_DIGEST.test(proof.body_digest)
|
|
96
|
+
|| typeof proof.signature_b64u !== 'string' || !/^[A-Za-z0-9_-]+$/.test(proof.signature_b64u)) {
|
|
97
|
+
return refuse('artifact_signature_envelope_invalid');
|
|
98
|
+
}
|
|
99
|
+
if (riskDigest(body) !== proof.body_digest) return refuse('digest_mismatch');
|
|
100
|
+
const pin = trustedKeys?.[artifact.issuer.key_id];
|
|
101
|
+
if (!pin || pin.issuer_id !== artifact.issuer.id) return refuse('issuer_untrusted');
|
|
102
|
+
const keyBytes = Buffer.from(pin.public_key, 'base64url');
|
|
103
|
+
if (keyBytes.toString('base64url') !== pin.public_key) return refuse('pinned_key_invalid');
|
|
104
|
+
const key = crypto.createPublicKey({ key: keyBytes, type: 'spki', format: 'der' });
|
|
105
|
+
if (key.asymmetricKeyType !== 'ed25519') return refuse('pinned_key_invalid');
|
|
106
|
+
const signature = Buffer.from(proof.signature_b64u, 'base64url');
|
|
107
|
+
if (signature.length !== 64 || signature.toString('base64url') !== proof.signature_b64u
|
|
108
|
+
|| !crypto.verify(null, signingBytes(version, body), key, signature)) return refuse('signature_invalid');
|
|
109
|
+
return { valid: true, reason: null, body, artifact_digest: riskDigest(artifact) };
|
|
110
|
+
} catch {
|
|
111
|
+
return refuse('artifact_invalid');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
package/src/remedy-program.ts
CHANGED
|
@@ -1035,61 +1035,70 @@ export function createRemedyProgramKernel(options: RemedyProgramKernelConfig) {
|
|
|
1035
1035
|
const loaded = await load(value.tenantId, value.instanceId);
|
|
1036
1036
|
if (!loaded.ok) return loaded;
|
|
1037
1037
|
const state = loaded.state as DataRecord;
|
|
1038
|
+
const decided = (ok: boolean, reason?: string, fields: DataRecord = {}) => pass({
|
|
1039
|
+
ok,
|
|
1040
|
+
...(reason ? { reason } : {}),
|
|
1041
|
+
program_digest: state.remedy_profile_digest,
|
|
1042
|
+
...fields,
|
|
1043
|
+
}) as RemedyProgramResult;
|
|
1038
1044
|
const requestDigest = digest(value);
|
|
1039
|
-
if (state.status === 'original_proved_no_effect') return
|
|
1045
|
+
if (state.status === 'original_proved_no_effect') return decided(false, 'remedy_case_terminal');
|
|
1040
1046
|
if (state.original.outcome === 'indeterminate'
|
|
1041
1047
|
&& state.original_reconciliation?.outcome !== 'executed') {
|
|
1042
|
-
return
|
|
1048
|
+
return decided(false, 'original_effect_indeterminate');
|
|
1043
1049
|
}
|
|
1044
1050
|
const priorOperation = allAttempts(state).find((attempt) => (
|
|
1045
1051
|
attempt.remedy_operation_id === value.authorization.remedy_operation_id
|
|
1046
1052
|
));
|
|
1047
1053
|
if (priorOperation) {
|
|
1048
1054
|
return priorOperation.request_digest === requestDigest
|
|
1049
|
-
?
|
|
1050
|
-
:
|
|
1055
|
+
? decided(true, undefined, { idempotent: true, state })
|
|
1056
|
+
: decided(false, 'remedy_operation_replayed');
|
|
1057
|
+
}
|
|
1058
|
+
if (state.remaining_units === 0) return decided(false, 'remedy_limit_exhausted');
|
|
1059
|
+
if (['remedied', 'resolved_no_remedy'].includes(state.status)) {
|
|
1060
|
+
return decided(false, 'remedy_case_terminal');
|
|
1051
1061
|
}
|
|
1052
|
-
if (state.remaining_units === 0) return fail('remedy_limit_exhausted');
|
|
1053
|
-
if (['remedied', 'resolved_no_remedy'].includes(state.status)) return fail('remedy_case_terminal');
|
|
1054
1062
|
if (state.active_remedy !== null) {
|
|
1055
1063
|
return state.status === 'remedy_indeterminate'
|
|
1056
|
-
?
|
|
1064
|
+
? decided(false, 'remedy_indeterminate')
|
|
1065
|
+
: decided(false, 'remedy_already_active');
|
|
1057
1066
|
}
|
|
1058
1067
|
if (!['disputed', 'partially_remedied'].includes(state.status) || state.dispute === null) {
|
|
1059
|
-
return
|
|
1068
|
+
return decided(false, 'remedy_case_unavailable');
|
|
1060
1069
|
}
|
|
1061
1070
|
if (evidenceUsed(state, value.authorization.evidence_id, value.authorization.evidence_digest)) {
|
|
1062
|
-
return
|
|
1071
|
+
return decided(false, 'evidence_replayed');
|
|
1063
1072
|
}
|
|
1064
1073
|
const at = transitionTime(state);
|
|
1065
|
-
if (typeof at !== 'number') return at;
|
|
1074
|
+
if (typeof at !== 'number') return decided(false, at.reason);
|
|
1066
1075
|
if (instant(value.authorization.authorized_at) > at
|
|
1067
1076
|
|| instant(value.authorization.authorized_at) < instant(state.dispute.opened_at)) {
|
|
1068
|
-
return
|
|
1077
|
+
return decided(false, 'remedy_authorization_time_invalid');
|
|
1069
1078
|
}
|
|
1070
1079
|
const disputeRemaining = state.dispute.requested_units - state.remedied_units;
|
|
1071
1080
|
if (value.authorization.units > state.remaining_units || value.authorization.units > disputeRemaining) {
|
|
1072
|
-
return
|
|
1081
|
+
return decided(false, 'remedy_limit_exceeded');
|
|
1073
1082
|
}
|
|
1074
1083
|
if (value.authorization.remedy_operation_id === state.original.operation_id
|
|
1075
1084
|
|| value.authorization.remedy_action_digest === state.original.action_digest) {
|
|
1076
|
-
return
|
|
1085
|
+
return decided(false, 'remedy_must_be_compensating');
|
|
1077
1086
|
}
|
|
1078
1087
|
const verified = await invoke(verifyRemedyAuthorization, {
|
|
1079
1088
|
authorization: clone(value.authorization), expected: expectedContext(state),
|
|
1080
1089
|
});
|
|
1081
1090
|
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_AUTHORIZATION_KEYS)) {
|
|
1082
|
-
return
|
|
1091
|
+
return decided(false, 'remedy_authorization_invalid');
|
|
1083
1092
|
}
|
|
1084
1093
|
if (!validRemedyOwner(verified as DataRecord)) {
|
|
1085
|
-
return
|
|
1094
|
+
return decided(false, 'remedy_owner_invalid');
|
|
1086
1095
|
}
|
|
1087
1096
|
if ([...AUTHORIZATION_KEYS].some((key) => !same(verified[key], value.authorization[key]))
|
|
1088
1097
|
|| verified.dispute_id !== state.dispute.dispute_id
|
|
1089
1098
|
|| verified.original_operation_id !== state.original.operation_id
|
|
1090
1099
|
|| verified.destination_binding_digest !== state.destination_binding_digest
|
|
1091
1100
|
|| verified.unit !== state.unit) {
|
|
1092
|
-
return
|
|
1101
|
+
return decided(false, 'remedy_authorization_binding_mismatch');
|
|
1093
1102
|
}
|
|
1094
1103
|
const next = touch(state, at);
|
|
1095
1104
|
next.status = 'remedy_authorized';
|
|
@@ -1108,7 +1117,9 @@ export function createRemedyProgramKernel(options: RemedyProgramKernelConfig) {
|
|
|
1108
1117
|
};
|
|
1109
1118
|
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
1110
1119
|
const committed = await commit(state, next);
|
|
1111
|
-
return committed.ok
|
|
1120
|
+
return committed.ok
|
|
1121
|
+
? decided(true, undefined, { state: committed.state })
|
|
1122
|
+
: decided(false, committed.reason);
|
|
1112
1123
|
}
|
|
1113
1124
|
|
|
1114
1125
|
async function claimRemedy(input: unknown): Promise<RemedyProgramResult> {
|