@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,701 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Deployment-bound PostgreSQL adapter for the Open Exposure Ledger.
|
|
4
|
+
*
|
|
5
|
+
* Each mutation is one PostgreSQL function call. Authentication is the
|
|
6
|
+
* PostgreSQL session principal plus its private tenant/authority mapping; the
|
|
7
|
+
* application credential field is deliberately never serialized to SQL.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
OPEN_EXPOSURE_HISTORY_VERSION,
|
|
14
|
+
OPEN_EXPOSURE_LEDGER_VERSION,
|
|
15
|
+
type OpenExposureAgingInput,
|
|
16
|
+
type OpenExposureAuth,
|
|
17
|
+
type OpenExposureBeginInput,
|
|
18
|
+
type OpenExposureBeginResult,
|
|
19
|
+
type OpenExposureBreakdown,
|
|
20
|
+
type OpenExposureCeiling,
|
|
21
|
+
type OpenExposureCeilingInput,
|
|
22
|
+
type OpenExposureCeilingResult,
|
|
23
|
+
type OpenExposureDeadlineInput,
|
|
24
|
+
type OpenExposureHistoryEntry,
|
|
25
|
+
type OpenExposureHistoryResult,
|
|
26
|
+
type OpenExposureIndeterminateInput,
|
|
27
|
+
type OpenExposureLedger,
|
|
28
|
+
type OpenExposureListResult,
|
|
29
|
+
type OpenExposureReadResult,
|
|
30
|
+
type OpenExposureReconciliationInput,
|
|
31
|
+
type OpenExposureRecord,
|
|
32
|
+
type OpenExposureRecordResult,
|
|
33
|
+
type OpenExposureReference,
|
|
34
|
+
type OpenExposureRefusal,
|
|
35
|
+
type OpenExposureRefusalReason,
|
|
36
|
+
type OpenExposureReserveInput,
|
|
37
|
+
type OpenExposureRole,
|
|
38
|
+
type OpenExposureStatus,
|
|
39
|
+
type OpenExposureSumInput,
|
|
40
|
+
type OpenExposureSumResult,
|
|
41
|
+
} from './open-exposure-ledger.js';
|
|
42
|
+
|
|
43
|
+
export interface OpenExposurePostgresQueryResult {
|
|
44
|
+
rowCount: number;
|
|
45
|
+
rows?: Array<Record<string, unknown>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type OpenExposurePostgresQuery = (
|
|
49
|
+
text: string,
|
|
50
|
+
params: readonly unknown[],
|
|
51
|
+
) => Promise<OpenExposurePostgresQueryResult>;
|
|
52
|
+
|
|
53
|
+
export interface CreateOpenExposurePostgresLedgerOptions {
|
|
54
|
+
query: OpenExposurePostgresQuery;
|
|
55
|
+
tenantId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface OpenExposurePostgresLedger extends OpenExposureLedger {
|
|
59
|
+
readonly durable: true;
|
|
60
|
+
readonly deploymentBound: true;
|
|
61
|
+
readonly singleTenant: true;
|
|
62
|
+
readonly authentication: 'postgres_session_principal';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const OPEN_EXPOSURE_POSTGRES_SQL = Object.freeze({
|
|
66
|
+
registerCeiling: 'SELECT open_exposure_private.register_ceiling($1::jsonb) AS result',
|
|
67
|
+
reserve: 'SELECT open_exposure_private.reserve($1::jsonb) AS result',
|
|
68
|
+
beginInvocation: 'SELECT open_exposure_private.begin_invocation($1::jsonb) AS result',
|
|
69
|
+
markIndeterminate: 'SELECT open_exposure_private.mark_indeterminate($1::jsonb) AS result',
|
|
70
|
+
reconcile: 'SELECT open_exposure_private.reconcile($1::jsonb) AS result',
|
|
71
|
+
read: 'SELECT open_exposure_private.read_exposure($1::jsonb) AS result',
|
|
72
|
+
history: 'SELECT open_exposure_private.read_history($1::jsonb) AS result',
|
|
73
|
+
sumOpen: 'SELECT open_exposure_private.sum_open($1::jsonb) AS result',
|
|
74
|
+
listAging: 'SELECT open_exposure_private.list_aging($1::jsonb) AS result',
|
|
75
|
+
listDeadlines: 'SELECT open_exposure_private.list_deadlines($1::jsonb) AS result',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export class OpenExposurePostgresProtocolError extends Error {
|
|
79
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
80
|
+
super(message, options);
|
|
81
|
+
this.name = 'OpenExposurePostgresProtocolError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
86
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,511}$/;
|
|
87
|
+
const CURRENCY = /^[A-Z]{3}$/;
|
|
88
|
+
const OPERATION_TOKEN = /^open-exposure-op:v1:[A-Za-z0-9_-]{32,128}$/;
|
|
89
|
+
const RECONCILIATION_TOKEN = /^open-exposure-reconcile:v1:[A-Za-z0-9_-]{32,128}$/;
|
|
90
|
+
const INVOCATION_PERMIT = /^open-exposure-invoke:v1:[0-9a-f]{64}$/;
|
|
91
|
+
const CAID = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
92
|
+
const STATUSES = new Set<OpenExposureStatus>([
|
|
93
|
+
'RESERVED', 'INVOKING', 'INDETERMINATE',
|
|
94
|
+
'CLOSED_COMMITTED', 'CLOSED_PROVEN_NOT_COMMITTED',
|
|
95
|
+
]);
|
|
96
|
+
const REASONS = new Set<OpenExposureRefusalReason>([
|
|
97
|
+
'unauthenticated', 'wrong_authority', 'authority_separation_required',
|
|
98
|
+
'ceiling_not_configured', 'ceiling_exceeded', 'ceiling_id_conflict',
|
|
99
|
+
'ceiling_scope_conflict', 'exposure_exists', 'exposure_not_found',
|
|
100
|
+
'operation_token_conflict', 'immutable_binding_conflict', 'invocation_expired',
|
|
101
|
+
'reconciliation_token_conflict',
|
|
102
|
+
'state_conflict', 'reconciliation_required', 'already_closed',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
type Jsonish = null | boolean | number | string | Jsonish[] | { [key: string]: Jsonish };
|
|
106
|
+
|
|
107
|
+
function plain(value: unknown): value is Record<string, unknown> {
|
|
108
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
109
|
+
const prototype = Object.getPrototypeOf(value);
|
|
110
|
+
return prototype === Object.prototype || prototype === null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function protocol(condition: unknown, message: string): asserts condition {
|
|
114
|
+
if (!condition) throw new OpenExposurePostgresProtocolError(message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function frozenCopy<T>(value: T): Readonly<T> {
|
|
118
|
+
const copy = structuredClone(value);
|
|
119
|
+
const freeze = (candidate: unknown): void => {
|
|
120
|
+
if (candidate !== null && typeof candidate === 'object' && !Object.isFrozen(candidate)) {
|
|
121
|
+
for (const child of Object.values(candidate)) freeze(child);
|
|
122
|
+
Object.freeze(candidate);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
freeze(copy);
|
|
126
|
+
return copy;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function canonical(value: Jsonish): string {
|
|
130
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
131
|
+
return JSON.stringify(value);
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === 'number') {
|
|
134
|
+
protocol(Number.isFinite(value), 'cannot hash a non-finite number');
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
138
|
+
protocol(plain(value), 'cannot hash a non-plain object');
|
|
139
|
+
return `{${Object.keys(value).sort().map((key) => (
|
|
140
|
+
`${JSON.stringify(key)}:${canonical(value[key] as Jsonish)}`
|
|
141
|
+
)).join(',')}}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function digest(domain: string, value: Jsonish): string {
|
|
145
|
+
return `sha256:${crypto.createHash('sha256')
|
|
146
|
+
.update(domain)
|
|
147
|
+
.update('\0')
|
|
148
|
+
.update(canonical(value))
|
|
149
|
+
.digest('hex')}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function tokenDigest(token: string): string {
|
|
153
|
+
return digest('EP-OPEN-EXPOSURE-TOKEN-v1', token);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function text(value: unknown, field: string, pattern = IDENTIFIER): string {
|
|
157
|
+
protocol(typeof value === 'string' && pattern.test(value), `database returned invalid ${field}`);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nullableText(value: unknown, field: string, pattern = IDENTIFIER): string | null {
|
|
162
|
+
if (value === null) return null;
|
|
163
|
+
return text(value, field, pattern);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function iso(value: unknown, field: string): string {
|
|
167
|
+
protocol(typeof value === 'string', `database returned invalid ${field}`);
|
|
168
|
+
const parsed = Date.parse(value);
|
|
169
|
+
protocol(Number.isFinite(parsed) && new Date(parsed).toISOString() === value,
|
|
170
|
+
`database returned non-canonical ${field}`);
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function integer(value: unknown, field: string): number {
|
|
175
|
+
const candidate = typeof value === 'string' ? Number(value) : value;
|
|
176
|
+
protocol(Number.isSafeInteger(candidate) && Number(candidate) >= 0,
|
|
177
|
+
`database returned invalid ${field}`);
|
|
178
|
+
return Number(candidate);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function minor(value: unknown, field: string, allowZero = false): bigint {
|
|
182
|
+
protocol(typeof value === 'string' && /^(?:0|[1-9][0-9]{0,18})$/.test(value),
|
|
183
|
+
`database returned invalid ${field}`);
|
|
184
|
+
const parsed = BigInt(value);
|
|
185
|
+
protocol(parsed <= 9_223_372_036_854_775_807n && (allowZero ? parsed >= 0n : parsed > 0n),
|
|
186
|
+
`database returned out-of-range ${field}`);
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function digestArray(value: unknown, field: string): readonly string[] {
|
|
191
|
+
protocol(Array.isArray(value), `database returned invalid ${field}`);
|
|
192
|
+
const parsed = value.map((entry, index) => text(entry, `${field}[${index}]`, SHA256));
|
|
193
|
+
protocol(new Set(parsed).size === parsed.length, `database returned duplicate ${field}`);
|
|
194
|
+
return frozenCopy(parsed);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseCeiling(value: unknown): Readonly<OpenExposureCeiling> {
|
|
198
|
+
protocol(plain(value), 'database returned invalid ceiling');
|
|
199
|
+
protocol(value.version === OPEN_EXPOSURE_LEDGER_VERSION, 'database returned unsupported ceiling version');
|
|
200
|
+
protocol(['TENANT', 'PROGRAM', 'COUNTERPARTY', 'ACTION_CLASS'].includes(String(value.scope)),
|
|
201
|
+
'database returned invalid ceiling scope');
|
|
202
|
+
return frozenCopy({
|
|
203
|
+
'@version': OPEN_EXPOSURE_LEDGER_VERSION,
|
|
204
|
+
tenantId: text(value.tenant_id, 'ceiling tenant_id'),
|
|
205
|
+
ceilingId: text(value.ceiling_id, 'ceiling ceiling_id'),
|
|
206
|
+
scope: value.scope as OpenExposureCeiling['scope'],
|
|
207
|
+
scopeValue: value.scope === 'TENANT' && value.scope_value === '*'
|
|
208
|
+
? '*'
|
|
209
|
+
: text(value.scope_value, 'ceiling scope_value'),
|
|
210
|
+
currency: text(value.currency, 'ceiling currency', CURRENCY),
|
|
211
|
+
windowStart: iso(value.window_start, 'ceiling window_start'),
|
|
212
|
+
windowEnd: iso(value.window_end, 'ceiling window_end'),
|
|
213
|
+
limitMinor: minor(value.limit_minor, 'ceiling limit_minor', true),
|
|
214
|
+
policyDigest: text(value.policy_digest, 'ceiling policy_digest', SHA256),
|
|
215
|
+
ceilingDigest: text(value.ceiling_digest, 'ceiling ceiling_digest', SHA256),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseRecord(value: unknown): Readonly<OpenExposureRecord> {
|
|
220
|
+
protocol(plain(value), 'database returned invalid exposure record');
|
|
221
|
+
protocol(value.version === OPEN_EXPOSURE_LEDGER_VERSION, 'database returned unsupported record version');
|
|
222
|
+
protocol(typeof value.status === 'string' && STATUSES.has(value.status as OpenExposureStatus),
|
|
223
|
+
'database returned invalid exposure status');
|
|
224
|
+
protocol(value.reconciliation_outcome === null
|
|
225
|
+
|| ['COMMITTED', 'PROVEN_NOT_COMMITTED', 'INDETERMINATE'].includes(String(value.reconciliation_outcome)),
|
|
226
|
+
'database returned invalid reconciliation outcome');
|
|
227
|
+
return frozenCopy({
|
|
228
|
+
'@version': OPEN_EXPOSURE_LEDGER_VERSION,
|
|
229
|
+
tenantId: text(value.tenant_id, 'tenant_id'),
|
|
230
|
+
exposureId: text(value.exposure_id, 'exposure_id'),
|
|
231
|
+
operationTokenDigest: text(value.operation_token_digest, 'operation_token_digest', SHA256),
|
|
232
|
+
reservationDigest: text(value.reservation_digest, 'reservation_digest', SHA256),
|
|
233
|
+
programId: text(value.program_id, 'program_id'),
|
|
234
|
+
programVersion: text(value.program_version, 'program_version'),
|
|
235
|
+
programSourceDigest: text(value.program_source_digest, 'program_source_digest', SHA256),
|
|
236
|
+
programDigest: text(value.program_digest, 'program_digest', SHA256),
|
|
237
|
+
caid: text(value.caid, 'caid', CAID),
|
|
238
|
+
actionDigest: text(value.action_digest, 'action_digest', SHA256),
|
|
239
|
+
admissionSnapshotDigest: text(
|
|
240
|
+
value.admission_snapshot_digest, 'admission_snapshot_digest', SHA256,
|
|
241
|
+
),
|
|
242
|
+
authorizationDigest: text(value.authorization_digest, 'authorization_digest', SHA256),
|
|
243
|
+
authorizationExpiresAt: iso(value.authorization_expires_at, 'authorization_expires_at'),
|
|
244
|
+
counterpartyId: text(value.counterparty_id, 'counterparty_id'),
|
|
245
|
+
actionClass: text(value.action_class, 'action_class'),
|
|
246
|
+
amountMinor: minor(value.amount_minor, 'amount_minor'),
|
|
247
|
+
currency: text(value.currency, 'currency', CURRENCY),
|
|
248
|
+
windowStart: iso(value.window_start, 'window_start'),
|
|
249
|
+
windowEnd: iso(value.window_end, 'window_end'),
|
|
250
|
+
reservedAt: iso(value.reserved_at, 'reserved_at'),
|
|
251
|
+
invokeBy: iso(value.invoke_by, 'invoke_by'),
|
|
252
|
+
reconcileBy: iso(value.reconcile_by, 'reconcile_by'),
|
|
253
|
+
originAuthorityId: text(value.origin_authority_id, 'origin_authority_id'),
|
|
254
|
+
executorAuthorityId: text(value.executor_authority_id, 'executor_authority_id'),
|
|
255
|
+
reconciliationAuthorityId: text(value.reconciliation_authority_id, 'reconciliation_authority_id'),
|
|
256
|
+
reservationEvidenceDigest: text(
|
|
257
|
+
value.reservation_evidence_digest, 'reservation_evidence_digest', SHA256,
|
|
258
|
+
),
|
|
259
|
+
ceilingDigests: digestArray(value.ceiling_digests, 'ceiling_digests'),
|
|
260
|
+
revision: integer(value.revision, 'revision'),
|
|
261
|
+
status: value.status as OpenExposureStatus,
|
|
262
|
+
invokedAt: value.invoked_at === null ? null : iso(value.invoked_at, 'invoked_at'),
|
|
263
|
+
invocationPermitDigest: nullableText(
|
|
264
|
+
value.invocation_permit_digest, 'invocation_permit_digest', SHA256,
|
|
265
|
+
),
|
|
266
|
+
indeterminateEvidenceDigest: nullableText(
|
|
267
|
+
value.indeterminate_evidence_digest, 'indeterminate_evidence_digest', SHA256,
|
|
268
|
+
),
|
|
269
|
+
reconciliationOutcome: value.reconciliation_outcome as OpenExposureRecord['reconciliationOutcome'],
|
|
270
|
+
reconciliationEvidenceDigest: nullableText(
|
|
271
|
+
value.reconciliation_evidence_digest, 'reconciliation_evidence_digest', SHA256,
|
|
272
|
+
),
|
|
273
|
+
lastChangedAt: iso(value.last_changed_at, 'last_changed_at'),
|
|
274
|
+
predecessorRecordDigest: nullableText(
|
|
275
|
+
value.predecessor_record_digest, 'predecessor_record_digest', SHA256,
|
|
276
|
+
),
|
|
277
|
+
recordDigest: text(value.record_digest, 'record_digest', SHA256),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseHistoryEntry(value: unknown): Readonly<OpenExposureHistoryEntry> {
|
|
282
|
+
protocol(plain(value), 'database returned invalid history entry');
|
|
283
|
+
protocol(value.version === OPEN_EXPOSURE_HISTORY_VERSION, 'database returned unsupported history version');
|
|
284
|
+
protocol([
|
|
285
|
+
'RESERVED', 'INVOKING', 'INDETERMINATE', 'RECONCILED_INDETERMINATE',
|
|
286
|
+
'CLOSED_COMMITTED', 'CLOSED_PROVEN_NOT_COMMITTED',
|
|
287
|
+
].includes(String(value.event)), 'database returned invalid history event');
|
|
288
|
+
return frozenCopy({
|
|
289
|
+
'@version': OPEN_EXPOSURE_HISTORY_VERSION,
|
|
290
|
+
tenantId: text(value.tenant_id, 'history tenant_id'),
|
|
291
|
+
exposureId: text(value.exposure_id, 'history exposure_id'),
|
|
292
|
+
sequence: integer(value.sequence, 'history sequence'),
|
|
293
|
+
event: value.event as OpenExposureHistoryEntry['event'],
|
|
294
|
+
programVersion: text(value.program_version, 'history program_version'),
|
|
295
|
+
programSourceDigest: text(
|
|
296
|
+
value.program_source_digest, 'history program_source_digest', SHA256,
|
|
297
|
+
),
|
|
298
|
+
programDigest: text(value.program_digest, 'history program_digest', SHA256),
|
|
299
|
+
caid: text(value.caid, 'history caid', CAID),
|
|
300
|
+
actionDigest: text(value.action_digest, 'history action_digest', SHA256),
|
|
301
|
+
admissionSnapshotDigest: text(
|
|
302
|
+
value.admission_snapshot_digest, 'history admission_snapshot_digest', SHA256,
|
|
303
|
+
),
|
|
304
|
+
authorizationDigest: text(
|
|
305
|
+
value.authorization_digest, 'history authorization_digest', SHA256,
|
|
306
|
+
),
|
|
307
|
+
authorizationExpiresAt: iso(
|
|
308
|
+
value.authorization_expires_at, 'history authorization_expires_at',
|
|
309
|
+
),
|
|
310
|
+
invocationPermitDigest: nullableText(
|
|
311
|
+
value.invocation_permit_digest, 'history invocation_permit_digest', SHA256,
|
|
312
|
+
),
|
|
313
|
+
recordDigest: text(value.record_digest, 'history record_digest', SHA256),
|
|
314
|
+
evidenceDigest: text(value.evidence_digest, 'history evidence_digest', SHA256),
|
|
315
|
+
recordedAt: iso(value.recorded_at, 'history recorded_at'),
|
|
316
|
+
predecessorEntryDigest: nullableText(
|
|
317
|
+
value.predecessor_entry_digest, 'history predecessor_entry_digest', SHA256,
|
|
318
|
+
),
|
|
319
|
+
entryDigest: text(value.entry_digest, 'history entry_digest', SHA256),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseBreakdowns(value: unknown, field: string): readonly Readonly<OpenExposureBreakdown>[] {
|
|
324
|
+
protocol(Array.isArray(value), `database returned invalid ${field}`);
|
|
325
|
+
const parsed = value.map((entry) => {
|
|
326
|
+
protocol(plain(entry), `database returned invalid ${field} entry`);
|
|
327
|
+
return { key: text(entry.key, `${field} key`), amountMinor: minor(entry.amount_minor, `${field} amount`, true) };
|
|
328
|
+
});
|
|
329
|
+
for (let index = 1; index < parsed.length; index += 1) {
|
|
330
|
+
protocol(parsed[index - 1].key < parsed[index].key, `database returned unsorted ${field}`);
|
|
331
|
+
}
|
|
332
|
+
return frozenCopy(parsed);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseRefusal(value: Record<string, unknown>): OpenExposureRefusal {
|
|
336
|
+
protocol(value.ok === false && typeof value.reason === 'string'
|
|
337
|
+
&& REASONS.has(value.reason as OpenExposureRefusalReason),
|
|
338
|
+
'database returned invalid refusal');
|
|
339
|
+
return { ok: false, reason: value.reason as OpenExposureRefusalReason };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function wireAuth(auth: OpenExposureAuth): Record<string, string> {
|
|
343
|
+
protocol(auth && typeof auth === 'object', 'auth is required');
|
|
344
|
+
protocol(['POLICY_ADMIN', 'ORIGIN', 'EXECUTOR', 'RECONCILER', 'READER'].includes(auth.role),
|
|
345
|
+
'auth role is invalid');
|
|
346
|
+
protocol(IDENTIFIER.test(auth.authorityId), 'auth authorityId is invalid');
|
|
347
|
+
return { authority_kind: auth.role, authority_id: auth.authorityId };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function validateBindingInput(input: OpenExposureBeginInput | OpenExposureReserveInput): void {
|
|
351
|
+
protocol(IDENTIFIER.test(input.programVersion), 'programVersion is invalid');
|
|
352
|
+
protocol(CAID.test(input.caid), 'caid is invalid');
|
|
353
|
+
for (const [field, value] of Object.entries({
|
|
354
|
+
programSourceDigest: input.programSourceDigest,
|
|
355
|
+
programDigest: input.programDigest,
|
|
356
|
+
actionDigest: input.actionDigest,
|
|
357
|
+
admissionSnapshotDigest: input.admissionSnapshotDigest,
|
|
358
|
+
authorizationDigest: input.authorizationDigest,
|
|
359
|
+
})) protocol(SHA256.test(value), `${field} is invalid`);
|
|
360
|
+
const authorizationExpiresAt = Date.parse(input.authorizationExpiresAt);
|
|
361
|
+
protocol(Number.isFinite(authorizationExpiresAt)
|
|
362
|
+
&& new Date(authorizationExpiresAt).toISOString() === input.authorizationExpiresAt,
|
|
363
|
+
'authorizationExpiresAt is invalid');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function wireBinding(
|
|
367
|
+
input: OpenExposureBeginInput | OpenExposureReserveInput,
|
|
368
|
+
): Record<string, string> {
|
|
369
|
+
validateBindingInput(input);
|
|
370
|
+
return {
|
|
371
|
+
program_version: input.programVersion,
|
|
372
|
+
program_source_digest: input.programSourceDigest,
|
|
373
|
+
program_digest: input.programDigest,
|
|
374
|
+
caid: input.caid,
|
|
375
|
+
action_digest: input.actionDigest,
|
|
376
|
+
admission_snapshot_digest: input.admissionSnapshotDigest,
|
|
377
|
+
authorization_digest: input.authorizationDigest,
|
|
378
|
+
authorization_expires_at: input.authorizationExpiresAt,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function wireReservation(input: OpenExposureReserveInput, auth: OpenExposureAuth): Record<string, Jsonish> {
|
|
383
|
+
protocol(OPERATION_TOKEN.test(input.operationToken), 'operationToken is invalid');
|
|
384
|
+
protocol(typeof input.amountMinor === 'bigint' && input.amountMinor > 0n,
|
|
385
|
+
'amountMinor is invalid');
|
|
386
|
+
const invokeBy = Date.parse(input.invokeBy);
|
|
387
|
+
const windowEnd = Date.parse(input.windowEnd);
|
|
388
|
+
const authorizationExpiresAt = Date.parse(input.authorizationExpiresAt);
|
|
389
|
+
protocol(Number.isFinite(invokeBy) && new Date(invokeBy).toISOString() === input.invokeBy,
|
|
390
|
+
'invokeBy is invalid');
|
|
391
|
+
protocol(Number.isFinite(windowEnd) && new Date(windowEnd).toISOString() === input.windowEnd,
|
|
392
|
+
'windowEnd is invalid');
|
|
393
|
+
protocol(invokeBy <= windowEnd && invokeBy <= authorizationExpiresAt,
|
|
394
|
+
'invokeBy exceeds its immutable bound');
|
|
395
|
+
const operationTokenDigest = tokenDigest(input.operationToken);
|
|
396
|
+
const body: Record<string, Jsonish> = {
|
|
397
|
+
version: OPEN_EXPOSURE_LEDGER_VERSION,
|
|
398
|
+
tenant_id: input.tenantId,
|
|
399
|
+
exposure_id: input.exposureId,
|
|
400
|
+
operation_token_digest: operationTokenDigest,
|
|
401
|
+
program_id: input.programId,
|
|
402
|
+
...wireBinding(input),
|
|
403
|
+
counterparty_id: input.counterpartyId,
|
|
404
|
+
action_class: input.actionClass,
|
|
405
|
+
amount_minor: input.amountMinor.toString(10),
|
|
406
|
+
currency: input.currency,
|
|
407
|
+
window_start: input.windowStart,
|
|
408
|
+
window_end: input.windowEnd,
|
|
409
|
+
reserved_at: input.reservedAt,
|
|
410
|
+
invoke_by: input.invokeBy,
|
|
411
|
+
reconcile_by: input.reconcileBy,
|
|
412
|
+
origin_authority_id: input.originAuthorityId,
|
|
413
|
+
executor_authority_id: input.executorAuthorityId,
|
|
414
|
+
reconciliation_authority_id: input.reconciliationAuthorityId,
|
|
415
|
+
reservation_evidence_digest: input.reservationEvidenceDigest,
|
|
416
|
+
...wireAuth(auth),
|
|
417
|
+
};
|
|
418
|
+
return {
|
|
419
|
+
...body,
|
|
420
|
+
reservation_digest: digest('EP-OPEN-EXPOSURE-RESERVATION-WIRE-v1', body),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function wireCeiling(input: OpenExposureCeilingInput, auth: OpenExposureAuth): Record<string, Jsonish> {
|
|
425
|
+
protocol(typeof input.limitMinor === 'bigint' && input.limitMinor >= 0n,
|
|
426
|
+
'limitMinor is invalid');
|
|
427
|
+
const body: Record<string, Jsonish> = {
|
|
428
|
+
version: OPEN_EXPOSURE_LEDGER_VERSION,
|
|
429
|
+
tenant_id: input.tenantId,
|
|
430
|
+
ceiling_id: input.ceilingId,
|
|
431
|
+
scope: input.scope,
|
|
432
|
+
scope_value: input.scopeValue,
|
|
433
|
+
currency: input.currency,
|
|
434
|
+
window_start: input.windowStart,
|
|
435
|
+
window_end: input.windowEnd,
|
|
436
|
+
limit_minor: input.limitMinor.toString(10),
|
|
437
|
+
policy_digest: input.policyDigest,
|
|
438
|
+
...wireAuth(auth),
|
|
439
|
+
};
|
|
440
|
+
return { ...body, ceiling_digest: digest('EP-OPEN-EXPOSURE-CEILING-WIRE-v1', body) };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function roleRefusal(auth: OpenExposureAuth, expected: OpenExposureRole): OpenExposureRefusal | null {
|
|
444
|
+
return auth.role === expected ? null : { ok: false, reason: 'wrong_authority' };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function createOpenExposurePostgresLedger(
|
|
448
|
+
options: CreateOpenExposurePostgresLedgerOptions,
|
|
449
|
+
): OpenExposurePostgresLedger {
|
|
450
|
+
protocol(options && typeof options.query === 'function', 'query is required');
|
|
451
|
+
protocol(typeof options.tenantId === 'string' && IDENTIFIER.test(options.tenantId),
|
|
452
|
+
'tenantId is invalid');
|
|
453
|
+
|
|
454
|
+
function tenantRefusal(tenantId: string): OpenExposureRefusal | null {
|
|
455
|
+
return tenantId === options.tenantId ? null : { ok: false, reason: 'unauthenticated' };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function rpc(sql: string, payload: Record<string, Jsonish>): Promise<Record<string, unknown>> {
|
|
459
|
+
const result = await options.query(sql, [JSON.stringify(payload)]);
|
|
460
|
+
protocol(result && result.rowCount === 1 && Array.isArray(result.rows)
|
|
461
|
+
&& result.rows.length === 1 && plain(result.rows[0]),
|
|
462
|
+
'ledger RPC returned no single result row');
|
|
463
|
+
let raw = result.rows[0].result;
|
|
464
|
+
if (typeof raw === 'string') {
|
|
465
|
+
try {
|
|
466
|
+
raw = JSON.parse(raw);
|
|
467
|
+
} catch (cause) {
|
|
468
|
+
throw new OpenExposurePostgresProtocolError('ledger RPC returned invalid JSON', { cause });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
protocol(plain(raw), 'ledger RPC returned an invalid result envelope');
|
|
472
|
+
return raw;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function recordMutation(
|
|
476
|
+
sql: string,
|
|
477
|
+
payload: Record<string, Jsonish>,
|
|
478
|
+
): Promise<OpenExposureRecordResult> {
|
|
479
|
+
const envelope = await rpc(sql, payload);
|
|
480
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
481
|
+
protocol(envelope.ok === true && typeof envelope.replayed === 'boolean',
|
|
482
|
+
'ledger RPC returned invalid mutation result');
|
|
483
|
+
return {
|
|
484
|
+
ok: true,
|
|
485
|
+
replayed: envelope.replayed,
|
|
486
|
+
record: parseRecord(envelope.record),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function beginMutation(
|
|
491
|
+
payload: Record<string, Jsonish>,
|
|
492
|
+
): Promise<OpenExposureBeginResult> {
|
|
493
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.beginInvocation, payload);
|
|
494
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
495
|
+
protocol(envelope.ok === true && envelope.replayed === false,
|
|
496
|
+
'ledger RPC returned an authorizing invocation replay');
|
|
497
|
+
const invocationPermit = text(
|
|
498
|
+
envelope.invocation_permit, 'invocation_permit', INVOCATION_PERMIT,
|
|
499
|
+
);
|
|
500
|
+
return {
|
|
501
|
+
ok: true,
|
|
502
|
+
replayed: false,
|
|
503
|
+
invocationPermit,
|
|
504
|
+
record: parseRecord(envelope.record),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function referencePayload(
|
|
509
|
+
input: OpenExposureReference,
|
|
510
|
+
auth: OpenExposureAuth,
|
|
511
|
+
): Record<string, Jsonish> {
|
|
512
|
+
return {
|
|
513
|
+
version: OPEN_EXPOSURE_LEDGER_VERSION,
|
|
514
|
+
tenant_id: input.tenantId,
|
|
515
|
+
exposure_id: input.exposureId,
|
|
516
|
+
...wireAuth(auth),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
durable: true,
|
|
522
|
+
deploymentBound: true,
|
|
523
|
+
singleTenant: true,
|
|
524
|
+
authentication: 'postgres_session_principal',
|
|
525
|
+
atomicReserve: true,
|
|
526
|
+
appendOnlyHistory: true,
|
|
527
|
+
blindRelease: false,
|
|
528
|
+
reconciliationOnlyCloseout: true,
|
|
529
|
+
|
|
530
|
+
async registerCeiling(input, auth): Promise<OpenExposureCeilingResult> {
|
|
531
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
532
|
+
if (tenantDenied) return tenantDenied;
|
|
533
|
+
const roleDenied = roleRefusal(auth, 'POLICY_ADMIN');
|
|
534
|
+
if (roleDenied) return roleDenied;
|
|
535
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.registerCeiling, wireCeiling(input, auth));
|
|
536
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
537
|
+
protocol(envelope.ok === true && typeof envelope.replayed === 'boolean',
|
|
538
|
+
'ledger RPC returned invalid ceiling result');
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
replayed: envelope.replayed,
|
|
542
|
+
ceiling: parseCeiling(envelope.ceiling),
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
async reserve(input, auth) {
|
|
547
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
548
|
+
if (tenantDenied) return tenantDenied;
|
|
549
|
+
const roleDenied = roleRefusal(auth, 'ORIGIN');
|
|
550
|
+
if (roleDenied) return roleDenied;
|
|
551
|
+
if (auth.authorityId !== input.originAuthorityId) {
|
|
552
|
+
return { ok: false, reason: 'wrong_authority' };
|
|
553
|
+
}
|
|
554
|
+
if (new Set([
|
|
555
|
+
input.originAuthorityId,
|
|
556
|
+
input.executorAuthorityId,
|
|
557
|
+
input.reconciliationAuthorityId,
|
|
558
|
+
]).size !== 3) {
|
|
559
|
+
return { ok: false, reason: 'authority_separation_required' };
|
|
560
|
+
}
|
|
561
|
+
return recordMutation(OPEN_EXPOSURE_POSTGRES_SQL.reserve, wireReservation(input, auth));
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
async beginInvocation(input: OpenExposureBeginInput, auth) {
|
|
565
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
566
|
+
if (tenantDenied) return tenantDenied;
|
|
567
|
+
const roleDenied = roleRefusal(auth, 'EXECUTOR');
|
|
568
|
+
if (roleDenied) return roleDenied;
|
|
569
|
+
protocol(OPERATION_TOKEN.test(input.operationToken), 'operationToken is invalid');
|
|
570
|
+
return beginMutation({
|
|
571
|
+
...referencePayload(input, auth),
|
|
572
|
+
operation_token_digest: tokenDigest(input.operationToken),
|
|
573
|
+
...wireBinding(input),
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
async markIndeterminate(input: OpenExposureIndeterminateInput, auth) {
|
|
578
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
579
|
+
if (tenantDenied) return tenantDenied;
|
|
580
|
+
const roleDenied = roleRefusal(auth, 'EXECUTOR');
|
|
581
|
+
if (roleDenied) return roleDenied;
|
|
582
|
+
protocol(OPERATION_TOKEN.test(input.operationToken), 'operationToken is invalid');
|
|
583
|
+
return recordMutation(OPEN_EXPOSURE_POSTGRES_SQL.markIndeterminate, {
|
|
584
|
+
...referencePayload(input, auth),
|
|
585
|
+
operation_token_digest: tokenDigest(input.operationToken),
|
|
586
|
+
evidence_digest: input.evidenceDigest,
|
|
587
|
+
observed_at: input.observedAt,
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
async reconcile(input: OpenExposureReconciliationInput, auth) {
|
|
592
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
593
|
+
if (tenantDenied) return tenantDenied;
|
|
594
|
+
const roleDenied = roleRefusal(auth, 'RECONCILER');
|
|
595
|
+
if (roleDenied) return roleDenied;
|
|
596
|
+
protocol(OPERATION_TOKEN.test(input.operationToken), 'operationToken is invalid');
|
|
597
|
+
protocol(RECONCILIATION_TOKEN.test(input.reconciliationToken), 'reconciliationToken is invalid');
|
|
598
|
+
const reconciliationTokenDigest = tokenDigest(input.reconciliationToken);
|
|
599
|
+
const body: Record<string, Jsonish> = {
|
|
600
|
+
...referencePayload(input, auth),
|
|
601
|
+
operation_token_digest: tokenDigest(input.operationToken),
|
|
602
|
+
reconciliation_token_digest: reconciliationTokenDigest,
|
|
603
|
+
outcome: input.outcome,
|
|
604
|
+
evidence_digest: input.evidenceDigest,
|
|
605
|
+
observed_at: input.observedAt,
|
|
606
|
+
};
|
|
607
|
+
return recordMutation(OPEN_EXPOSURE_POSTGRES_SQL.reconcile, {
|
|
608
|
+
...body,
|
|
609
|
+
request_digest: digest('EP-OPEN-EXPOSURE-RECONCILIATION-WIRE-v1', body),
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
async read(input, auth): Promise<OpenExposureReadResult> {
|
|
614
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
615
|
+
if (tenantDenied) return tenantDenied;
|
|
616
|
+
const roleDenied = roleRefusal(auth, 'READER');
|
|
617
|
+
if (roleDenied) return roleDenied;
|
|
618
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.read, referencePayload(input, auth));
|
|
619
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
620
|
+
protocol(envelope.ok === true, 'ledger RPC returned invalid read result');
|
|
621
|
+
return { ok: true, record: envelope.record === null ? null : parseRecord(envelope.record) };
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
async history(input, auth): Promise<OpenExposureHistoryResult> {
|
|
625
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
626
|
+
if (tenantDenied) return tenantDenied;
|
|
627
|
+
const roleDenied = roleRefusal(auth, 'READER');
|
|
628
|
+
if (roleDenied) return roleDenied;
|
|
629
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.history, referencePayload(input, auth));
|
|
630
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
631
|
+
protocol(envelope.ok === true && Array.isArray(envelope.entries),
|
|
632
|
+
'ledger RPC returned invalid history result');
|
|
633
|
+
return { ok: true, entries: frozenCopy(envelope.entries.map(parseHistoryEntry)) };
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
async sumOpen(input: OpenExposureSumInput, auth): Promise<OpenExposureSumResult> {
|
|
637
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
638
|
+
if (tenantDenied) return tenantDenied;
|
|
639
|
+
const roleDenied = roleRefusal(auth, 'READER');
|
|
640
|
+
if (roleDenied) return roleDenied;
|
|
641
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.sumOpen, {
|
|
642
|
+
version: OPEN_EXPOSURE_LEDGER_VERSION,
|
|
643
|
+
tenant_id: input.tenantId,
|
|
644
|
+
currency: input.currency,
|
|
645
|
+
window_start: input.windowStart,
|
|
646
|
+
window_end: input.windowEnd,
|
|
647
|
+
program_id: input.programId ?? null,
|
|
648
|
+
counterparty_id: input.counterpartyId ?? null,
|
|
649
|
+
action_class: input.actionClass ?? null,
|
|
650
|
+
...wireAuth(auth),
|
|
651
|
+
});
|
|
652
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
653
|
+
protocol(envelope.ok === true, 'ledger RPC returned invalid sum result');
|
|
654
|
+
return {
|
|
655
|
+
ok: true,
|
|
656
|
+
totalMinor: minor(envelope.total_minor, 'total_minor', true),
|
|
657
|
+
byProgram: parseBreakdowns(envelope.by_program, 'by_program'),
|
|
658
|
+
byCounterparty: parseBreakdowns(envelope.by_counterparty, 'by_counterparty'),
|
|
659
|
+
byActionClass: parseBreakdowns(envelope.by_action_class, 'by_action_class'),
|
|
660
|
+
byStatus: parseBreakdowns(envelope.by_status, 'by_status'),
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
async listAging(input: OpenExposureAgingInput, auth): Promise<OpenExposureListResult> {
|
|
665
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
666
|
+
if (tenantDenied) return tenantDenied;
|
|
667
|
+
const roleDenied = roleRefusal(auth, 'READER');
|
|
668
|
+
if (roleDenied) return roleDenied;
|
|
669
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.listAging, {
|
|
670
|
+
version: OPEN_EXPOSURE_LEDGER_VERSION,
|
|
671
|
+
tenant_id: input.tenantId,
|
|
672
|
+
as_of: input.asOf,
|
|
673
|
+
minimum_age_ms: input.minimumAgeMs,
|
|
674
|
+
limit: input.limit,
|
|
675
|
+
...wireAuth(auth),
|
|
676
|
+
});
|
|
677
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
678
|
+
protocol(envelope.ok === true && Array.isArray(envelope.records),
|
|
679
|
+
'ledger RPC returned invalid aging result');
|
|
680
|
+
return { ok: true, records: frozenCopy(envelope.records.map(parseRecord)) };
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
async listDeadlines(input: OpenExposureDeadlineInput, auth): Promise<OpenExposureListResult> {
|
|
684
|
+
const tenantDenied = tenantRefusal(input.tenantId);
|
|
685
|
+
if (tenantDenied) return tenantDenied;
|
|
686
|
+
const roleDenied = roleRefusal(auth, 'READER');
|
|
687
|
+
if (roleDenied) return roleDenied;
|
|
688
|
+
const envelope = await rpc(OPEN_EXPOSURE_POSTGRES_SQL.listDeadlines, {
|
|
689
|
+
version: OPEN_EXPOSURE_LEDGER_VERSION,
|
|
690
|
+
tenant_id: input.tenantId,
|
|
691
|
+
due_at_or_before: input.dueAtOrBefore,
|
|
692
|
+
limit: input.limit,
|
|
693
|
+
...wireAuth(auth),
|
|
694
|
+
});
|
|
695
|
+
if (envelope.ok === false) return parseRefusal(envelope);
|
|
696
|
+
protocol(envelope.ok === true && Array.isArray(envelope.records),
|
|
697
|
+
'ledger RPC returned invalid deadline result');
|
|
698
|
+
return { ok: true, records: frozenCopy(envelope.records.map(parseRecord)) };
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
}
|