@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +64 -0
  3. package/action-refusal-statement.js +1 -0
  4. package/coverage-reconciliation-attestation.js +1 -0
  5. package/dist/action-refusal-statement.d.ts +109 -0
  6. package/dist/action-refusal-statement.d.ts.map +1 -0
  7. package/dist/action-refusal-statement.js +333 -0
  8. package/dist/action-refusal-statement.js.map +1 -0
  9. package/dist/coverage-reconciliation-attestation.d.ts +29 -0
  10. package/dist/coverage-reconciliation-attestation.d.ts.map +1 -0
  11. package/dist/coverage-reconciliation-attestation.js +111 -0
  12. package/dist/coverage-reconciliation-attestation.js.map +1 -0
  13. package/dist/gate-qualification-v2.d.ts +4 -0
  14. package/dist/gate-qualification-v2.d.ts.map +1 -1
  15. package/dist/gate-qualification-v2.js +39 -47
  16. package/dist/gate-qualification-v2.js.map +1 -1
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +7 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/loss-allocation-schedule.d.ts +53 -0
  22. package/dist/loss-allocation-schedule.d.ts.map +1 -0
  23. package/dist/loss-allocation-schedule.js +351 -0
  24. package/dist/loss-allocation-schedule.js.map +1 -0
  25. package/dist/loss-experience-feed.d.ts +82 -0
  26. package/dist/loss-experience-feed.d.ts.map +1 -0
  27. package/dist/loss-experience-feed.js +314 -0
  28. package/dist/loss-experience-feed.js.map +1 -0
  29. package/dist/open-exposure-ledger-postgres.d.ts +40 -0
  30. package/dist/open-exposure-ledger-postgres.d.ts.map +1 -0
  31. package/dist/open-exposure-ledger-postgres.js +577 -0
  32. package/dist/open-exposure-ledger-postgres.js.map +1 -0
  33. package/dist/open-exposure-ledger.d.ts +243 -0
  34. package/dist/open-exposure-ledger.d.ts.map +1 -0
  35. package/dist/open-exposure-ledger.js +794 -0
  36. package/dist/open-exposure-ledger.js.map +1 -0
  37. package/dist/receipt-census.d.ts +21 -0
  38. package/dist/receipt-census.d.ts.map +1 -0
  39. package/dist/receipt-census.js +162 -0
  40. package/dist/receipt-census.js.map +1 -0
  41. package/dist/reliance-program.d.ts +88 -0
  42. package/dist/reliance-program.d.ts.map +1 -0
  43. package/dist/reliance-program.js +437 -0
  44. package/dist/reliance-program.js.map +1 -0
  45. package/dist/reliance-risk-crypto.d.ts +28 -0
  46. package/dist/reliance-risk-crypto.d.ts.map +1 -0
  47. package/dist/reliance-risk-crypto.js +110 -0
  48. package/dist/reliance-risk-crypto.js.map +1 -0
  49. package/dist/remedy-program.d.ts.map +1 -1
  50. package/dist/remedy-program.js +28 -18
  51. package/dist/remedy-program.js.map +1 -1
  52. package/loss-allocation-schedule.js +1 -0
  53. package/loss-experience-feed.js +1 -0
  54. package/open-exposure-ledger-postgres.js +1 -0
  55. package/open-exposure-ledger.js +1 -0
  56. package/package.json +42 -2
  57. package/receipt-census.js +1 -0
  58. package/reliance-program.js +3 -0
  59. package/src/action-refusal-statement.ts +396 -0
  60. package/src/coverage-reconciliation-attestation.ts +107 -0
  61. package/src/gate-qualification-v2.ts +56 -46
  62. package/src/index.ts +7 -0
  63. package/src/loss-allocation-schedule.ts +427 -0
  64. package/src/loss-experience-feed.ts +398 -0
  65. package/src/open-exposure-ledger-postgres.ts +701 -0
  66. package/src/open-exposure-ledger.ts +1213 -0
  67. package/src/receipt-census.ts +163 -0
  68. package/src/reliance-program.ts +499 -0
  69. package/src/reliance-risk-crypto.ts +114 -0
  70. 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
+
@@ -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 fail('remedy_case_terminal');
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 fail('original_effect_indeterminate');
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
- ? pass({ ok: true, idempotent: true, state }) as RemedyProgramResult
1050
- : fail('remedy_operation_replayed');
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
- ? fail('remedy_indeterminate') : fail('remedy_already_active');
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 fail('remedy_case_unavailable');
1068
+ return decided(false, 'remedy_case_unavailable');
1060
1069
  }
1061
1070
  if (evidenceUsed(state, value.authorization.evidence_id, value.authorization.evidence_digest)) {
1062
- return fail('evidence_replayed');
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 fail('remedy_authorization_time_invalid');
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 fail('remedy_limit_exceeded');
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 fail('remedy_must_be_compensating');
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 fail('remedy_authorization_invalid');
1091
+ return decided(false, 'remedy_authorization_invalid');
1083
1092
  }
1084
1093
  if (!validRemedyOwner(verified as DataRecord)) {
1085
- return fail('remedy_owner_invalid');
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 fail('remedy_authorization_binding_mismatch');
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 ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
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> {