@oxy.so/contracts 1.0.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/accountGraph.js +489 -0
- package/dist/cjs/agency.js +439 -0
- package/dist/cjs/browserHub.js +215 -0
- package/dist/cjs/civic.js +163 -0
- package/dist/cjs/commonsSignIn.js +59 -0
- package/dist/cjs/deviceBoot.js +50 -0
- package/dist/cjs/deviceDirectory.js +189 -0
- package/dist/cjs/devicePairing.js +138 -0
- package/dist/cjs/deviceSession.js +164 -0
- package/dist/cjs/emailAgentContext.js +32 -0
- package/dist/cjs/followGraph.js +28 -0
- package/dist/cjs/identity.js +258 -0
- package/dist/cjs/inboxPush.js +24 -0
- package/dist/cjs/index.js +618 -0
- package/dist/cjs/inference/accountBilling.js +334 -0
- package/dist/cjs/inference/aliaModelRelease.js +262 -0
- package/dist/cjs/inference/attribution.js +106 -0
- package/dist/cjs/inference/catalogue.js +487 -0
- package/dist/cjs/inference/entitlement.js +217 -0
- package/dist/cjs/inference/errors.js +309 -0
- package/dist/cjs/inference/identifiers.js +224 -0
- package/dist/cjs/inference/inbox.js +105 -0
- package/dist/cjs/inference/modelDocumentation.js +433 -0
- package/dist/cjs/inference/money.js +188 -0
- package/dist/cjs/inference/priceVersion.js +110 -0
- package/dist/cjs/inference/providerConnection.js +455 -0
- package/dist/cjs/inference/request.js +477 -0
- package/dist/cjs/inference/routingPolicy.js +318 -0
- package/dist/cjs/inference/streamEvents.js +258 -0
- package/dist/cjs/inference/usage.js +329 -0
- package/dist/cjs/inference/version.js +105 -0
- package/dist/cjs/keyRecovery.js +91 -0
- package/dist/cjs/keyRotation.js +75 -0
- package/dist/cjs/links.js +68 -0
- package/dist/cjs/moderationReputation.js +298 -0
- package/dist/cjs/oauth.js +66 -0
- package/dist/cjs/oxyRecordTypes.js +71 -0
- package/dist/cjs/protocol.js +53 -0
- package/dist/cjs/recommendations.js +168 -0
- package/dist/cjs/reputation.js +297 -0
- package/dist/cjs/sessionStatus.js +121 -0
- package/dist/cjs/transparency.js +89 -0
- package/dist/cjs/updates.js +252 -0
- package/dist/cjs/userInvalidation.js +89 -0
- package/dist/cjs/userResponse.js +245 -0
- package/dist/cjs/username.js +290 -0
- package/dist/cjs/webauthn.js +71 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/accountGraph.js +480 -0
- package/dist/esm/agency.js +436 -0
- package/dist/esm/browserHub.js +212 -0
- package/dist/esm/civic.js +160 -0
- package/dist/esm/commonsSignIn.js +56 -0
- package/dist/esm/deviceBoot.js +47 -0
- package/dist/esm/deviceDirectory.js +186 -0
- package/dist/esm/devicePairing.js +135 -0
- package/dist/esm/deviceSession.js +161 -0
- package/dist/esm/emailAgentContext.js +29 -0
- package/dist/esm/followGraph.js +27 -0
- package/dist/esm/identity.js +255 -0
- package/dist/esm/inboxPush.js +21 -0
- package/dist/esm/index.js +172 -0
- package/dist/esm/inference/accountBilling.js +331 -0
- package/dist/esm/inference/aliaModelRelease.js +259 -0
- package/dist/esm/inference/attribution.js +103 -0
- package/dist/esm/inference/catalogue.js +484 -0
- package/dist/esm/inference/entitlement.js +214 -0
- package/dist/esm/inference/errors.js +306 -0
- package/dist/esm/inference/identifiers.js +221 -0
- package/dist/esm/inference/inbox.js +102 -0
- package/dist/esm/inference/modelDocumentation.js +430 -0
- package/dist/esm/inference/money.js +185 -0
- package/dist/esm/inference/priceVersion.js +107 -0
- package/dist/esm/inference/providerConnection.js +452 -0
- package/dist/esm/inference/request.js +474 -0
- package/dist/esm/inference/routingPolicy.js +315 -0
- package/dist/esm/inference/streamEvents.js +255 -0
- package/dist/esm/inference/usage.js +326 -0
- package/dist/esm/inference/version.js +102 -0
- package/dist/esm/keyRecovery.js +88 -0
- package/dist/esm/keyRotation.js +72 -0
- package/dist/esm/links.js +65 -0
- package/dist/esm/moderationReputation.js +295 -0
- package/dist/esm/oauth.js +63 -0
- package/dist/esm/oxyRecordTypes.js +68 -0
- package/dist/esm/protocol.js +50 -0
- package/dist/esm/recommendations.js +165 -0
- package/dist/esm/reputation.js +293 -0
- package/dist/esm/sessionStatus.js +118 -0
- package/dist/esm/transparency.js +86 -0
- package/dist/esm/updates.js +249 -0
- package/dist/esm/userInvalidation.js +85 -0
- package/dist/esm/userResponse.js +240 -0
- package/dist/esm/username.js +283 -0
- package/dist/esm/webauthn.js +68 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/accountGraph.d.ts +378 -0
- package/dist/types/agency.d.ts +2162 -0
- package/dist/types/browserHub.d.ts +856 -0
- package/dist/types/civic.d.ts +338 -0
- package/dist/types/commonsSignIn.d.ts +58 -0
- package/dist/types/deviceBoot.d.ts +74 -0
- package/dist/types/deviceDirectory.d.ts +1317 -0
- package/dist/types/devicePairing.d.ts +130 -0
- package/dist/types/deviceSession.d.ts +411 -0
- package/dist/types/emailAgentContext.d.ts +248 -0
- package/dist/types/followGraph.d.ts +150 -0
- package/dist/types/identity.d.ts +402 -0
- package/dist/types/inboxPush.d.ts +30 -0
- package/dist/types/index.d.ts +100 -0
- package/dist/types/inference/accountBilling.d.ts +738 -0
- package/dist/types/inference/aliaModelRelease.d.ts +609 -0
- package/dist/types/inference/attribution.d.ts +176 -0
- package/dist/types/inference/catalogue.d.ts +1618 -0
- package/dist/types/inference/entitlement.d.ts +519 -0
- package/dist/types/inference/errors.d.ts +242 -0
- package/dist/types/inference/identifiers.d.ts +182 -0
- package/dist/types/inference/inbox.d.ts +374 -0
- package/dist/types/inference/modelDocumentation.d.ts +1603 -0
- package/dist/types/inference/money.d.ts +185 -0
- package/dist/types/inference/priceVersion.d.ts +182 -0
- package/dist/types/inference/providerConnection.d.ts +968 -0
- package/dist/types/inference/request.d.ts +2800 -0
- package/dist/types/inference/routingPolicy.d.ts +616 -0
- package/dist/types/inference/streamEvents.d.ts +950 -0
- package/dist/types/inference/usage.d.ts +1164 -0
- package/dist/types/inference/version.d.ts +102 -0
- package/dist/types/keyRecovery.d.ts +138 -0
- package/dist/types/keyRotation.d.ts +103 -0
- package/dist/types/links.d.ts +96 -0
- package/dist/types/moderationReputation.d.ts +487 -0
- package/dist/types/oauth.d.ts +86 -0
- package/dist/types/oxyRecordTypes.d.ts +62 -0
- package/dist/types/protocol.d.ts +86 -0
- package/dist/types/recommendations.d.ts +542 -0
- package/dist/types/reputation.d.ts +457 -0
- package/dist/types/sessionStatus.d.ts +231 -0
- package/dist/types/transparency.d.ts +392 -0
- package/dist/types/updates.d.ts +545 -0
- package/dist/types/userInvalidation.d.ts +94 -0
- package/dist/types/userResponse.d.ts +1706 -0
- package/dist/types/username.d.ts +265 -0
- package/dist/types/webauthn.d.ts +77 -0
- package/package.json +87 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The account-scoped billing surface: who pays, what they hold, what bounds
|
|
4
|
+
* them, and how it reconciles against the payment processor.
|
|
5
|
+
*
|
|
6
|
+
* Every shape here is denominated in {@link exactDecimalSchema} — an exact
|
|
7
|
+
* decimal string — for the reason `money.ts` gives at length: a JS `number`
|
|
8
|
+
* cannot represent `0.1 + 0.2`, and a balance is the last place a rounding error
|
|
9
|
+
* should be allowed to accumulate silently.
|
|
10
|
+
*
|
|
11
|
+
* ## What is NOT here, and where it is instead
|
|
12
|
+
*
|
|
13
|
+
* The BALANCE and the BUDGETS both live at `/inference/reporting` (#972
|
|
14
|
+
* workstream 8), which owns the customer's view of what they hold and what
|
|
15
|
+
* bounds them, stamped `{source, consistency}` so a reader can always tell which
|
|
16
|
+
* kind of number they are looking at. This file declares who PAYS and on what
|
|
17
|
+
* TERMS, plus the records that sit behind those numbers — invoices, processor
|
|
18
|
+
* payments, auto-recharge attempts and reconciliation.
|
|
19
|
+
*
|
|
20
|
+
* The split is not cosmetic. A second balance shape here would be a second
|
|
21
|
+
* answer to one question, and the pair would disagree the day one of them stops
|
|
22
|
+
* accounting for an invoiced account's unused credit line — which is exactly the
|
|
23
|
+
* kind of drift a customer discovers before we do.
|
|
24
|
+
*
|
|
25
|
+
* ## Product entitlements are NOT here
|
|
26
|
+
*
|
|
27
|
+
* Alia plans, their monthly allowances and the API-credit product live in
|
|
28
|
+
* `entitlement.ts`, in a shape that cannot be added to a balance. #972 is
|
|
29
|
+
* explicit that confusing a product subscription with pay-as-you-go inference
|
|
30
|
+
* spend is the failure mode, so the two are separate types that share no field
|
|
31
|
+
* and no unit.
|
|
32
|
+
*
|
|
33
|
+
* ## Stripe appears only as a REFERENCE
|
|
34
|
+
*
|
|
35
|
+
* `externalRef` names a record in the processor's database; nothing in this file
|
|
36
|
+
* is derived from Stripe, and no shape here can carry a processor-computed
|
|
37
|
+
* balance. The epic's invariant is that Stripe is a payment and invoicing
|
|
38
|
+
* processor and not the authoritative usage ledger, so the reconciliation shapes
|
|
39
|
+
* below describe a COMPARISON between two independent records rather than an
|
|
40
|
+
* import of one into the other.
|
|
41
|
+
*
|
|
42
|
+
* Decided in: docs/adr/0014-account-billing-and-entitlements.md,
|
|
43
|
+
* docs/adr/0009-usage-reservation-and-settlement.md.
|
|
44
|
+
*/
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.reconciliationReportSchema = exports.reconciliationRunSchema = exports.reconciliationDiscrepancySchema = exports.reconciliationRunStatusSchema = exports.RECONCILIATION_RUN_STATUSES = exports.reconciliationDiscrepancyKindSchema = exports.RECONCILIATION_DISCREPANCY_KINDS = exports.autoRechargeAttemptSchema = exports.autoRechargeStatusSchema = exports.AUTO_RECHARGE_STATUSES = exports.externalPaymentSchema = exports.externalPaymentKindSchema = exports.EXTERNAL_PAYMENT_KINDS = exports.externalPaymentProviderSchema = exports.EXTERNAL_PAYMENT_PROVIDERS = exports.billingInvoiceSchema = exports.billingInvoiceStatusSchema = exports.BILLING_INVOICE_STATUSES = exports.accountBillingStateSchema = exports.billingProfileSchema = exports.autoRechargeSchema = exports.billingProfileStatusSchema = exports.BILLING_PROFILE_STATUSES = exports.billingModeSchema = exports.BILLING_MODES = void 0;
|
|
47
|
+
const zod_1 = require("zod");
|
|
48
|
+
const identifiers_1 = require("./identifiers");
|
|
49
|
+
const money_1 = require("./money");
|
|
50
|
+
/* -------------------------------------------------------------------------- */
|
|
51
|
+
/* Billing profile */
|
|
52
|
+
/* -------------------------------------------------------------------------- */
|
|
53
|
+
/**
|
|
54
|
+
* How an account pays.
|
|
55
|
+
*
|
|
56
|
+
* `prepaid` spends money it topped up in advance. `invoiced` draws against a
|
|
57
|
+
* credit limit and is billed in arrears — the enterprise shape. Both settle
|
|
58
|
+
* through the same ledger; the difference is only which bucket a reservation
|
|
59
|
+
* draws from once the prepaid and granted balances are exhausted.
|
|
60
|
+
*/
|
|
61
|
+
exports.BILLING_MODES = ['prepaid', 'invoiced'];
|
|
62
|
+
exports.billingModeSchema = zod_1.z.enum(exports.BILLING_MODES);
|
|
63
|
+
/** Whether the profile may currently spend at all. */
|
|
64
|
+
exports.BILLING_PROFILE_STATUSES = ['active', 'suspended', 'closed'];
|
|
65
|
+
exports.billingProfileStatusSchema = zod_1.z.enum(exports.BILLING_PROFILE_STATUSES);
|
|
66
|
+
/**
|
|
67
|
+
* Automatic top-up settings.
|
|
68
|
+
*
|
|
69
|
+
* An IMPLICATION rather than a biconditional, matching the column CHECK: the
|
|
70
|
+
* two amounts may be configured before the feature is switched on, but an
|
|
71
|
+
* `enabled` recharge with either missing is a setting that reads as "on" and can
|
|
72
|
+
* never fire — the shape a customer discovers only when their traffic stops.
|
|
73
|
+
*/
|
|
74
|
+
exports.autoRechargeSchema = zod_1.z
|
|
75
|
+
.object({
|
|
76
|
+
enabled: zod_1.z.boolean(),
|
|
77
|
+
/** Recharge when the spendable balance falls below this. */
|
|
78
|
+
threshold: money_1.exactDecimalSchema.optional(),
|
|
79
|
+
/** How much to add. */
|
|
80
|
+
amount: money_1.exactDecimalSchema.optional(),
|
|
81
|
+
})
|
|
82
|
+
.strict()
|
|
83
|
+
.refine((value) => !value.enabled || (value.threshold !== undefined && value.amount !== undefined), { message: 'an enabled auto-recharge must carry both a threshold and an amount' });
|
|
84
|
+
/**
|
|
85
|
+
* Which account pays for a workload, and on what terms.
|
|
86
|
+
*
|
|
87
|
+
* `accountId` is an Oxy account of ANY kind — personal, organization, project,
|
|
88
|
+
* bot or channel — because `Application.ownerAccountId` may be any of them. It
|
|
89
|
+
* is branded (`oxyAccountIdSchema`), so a delegated end-user id cannot be
|
|
90
|
+
* substituted for it anywhere in this contract.
|
|
91
|
+
*/
|
|
92
|
+
exports.billingProfileSchema = zod_1.z
|
|
93
|
+
.object({
|
|
94
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
95
|
+
schemaVersion: zod_1.z.literal(1),
|
|
96
|
+
accountId: identifiers_1.oxyAccountIdSchema,
|
|
97
|
+
currency: money_1.currencyCodeSchema,
|
|
98
|
+
billingMode: exports.billingModeSchema,
|
|
99
|
+
status: exports.billingProfileStatusSchema,
|
|
100
|
+
/**
|
|
101
|
+
* How far an `invoiced` account may draw before a reservation is refused.
|
|
102
|
+
* Always `'0'` for a `prepaid` account, where it is not consulted at all.
|
|
103
|
+
*/
|
|
104
|
+
creditLimit: money_1.exactDecimalSchema,
|
|
105
|
+
autoRecharge: exports.autoRechargeSchema,
|
|
106
|
+
createdAt: zod_1.z.string().datetime(),
|
|
107
|
+
updatedAt: zod_1.z.string().datetime(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
/* -------------------------------------------------------------------------- */
|
|
111
|
+
/* Billing state */
|
|
112
|
+
/* -------------------------------------------------------------------------- */
|
|
113
|
+
/**
|
|
114
|
+
* WHO pays for an account and on what TERMS. Deliberately not how much they
|
|
115
|
+
* have.
|
|
116
|
+
*
|
|
117
|
+
* The balance lives at `GET /inference/reporting/accounts/:accountId/balance`
|
|
118
|
+
* (#972 workstream 8), which reads `account_balances` and stamps every response
|
|
119
|
+
* with `{source: 'financial_ledger', consistency: 'authoritative'}`. Restating
|
|
120
|
+
* those amounts here would be a second answer to one question, and the failure
|
|
121
|
+
* mode of two balance endpoints is the pair disagreeing on the day one of them
|
|
122
|
+
* stops accounting for the credit line.
|
|
123
|
+
*
|
|
124
|
+
* What this shape adds, and what nothing else carries: `billingAccountId` and
|
|
125
|
+
* `inherited`. A project draws on the nearest ancestor that has a profile
|
|
126
|
+
* (ADR 0014), so a Console page has to be able to say "this project spends the
|
|
127
|
+
* organization's balance" — showing somebody else's money under a project's name
|
|
128
|
+
* with no indication whose it is would be worse than showing nothing.
|
|
129
|
+
*/
|
|
130
|
+
exports.accountBillingStateSchema = zod_1.z
|
|
131
|
+
.object({
|
|
132
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
133
|
+
schemaVersion: zod_1.z.literal(1),
|
|
134
|
+
accountId: identifiers_1.oxyAccountIdSchema,
|
|
135
|
+
billingAccountId: identifiers_1.oxyAccountIdSchema,
|
|
136
|
+
inherited: zod_1.z.boolean(),
|
|
137
|
+
profile: exports.billingProfileSchema,
|
|
138
|
+
})
|
|
139
|
+
.strict();
|
|
140
|
+
/*
|
|
141
|
+
* SPENDING LIMITS AND THEIR ALERTS ARE NOT DECLARED HERE.
|
|
142
|
+
*
|
|
143
|
+
* `/inference/reporting` owns the budget surface (#972 workstream 8) and
|
|
144
|
+
* declares its own shapes beside the router that serves them, stamped
|
|
145
|
+
* `{source, consistency}` so a reader can always tell which kind of number they
|
|
146
|
+
* are looking at. A second set here would be a second answer to what a budget
|
|
147
|
+
* is, on a table both would write — and the closed alert-threshold set already
|
|
148
|
+
* lives once more where it is ENFORCED, as a CHECK on
|
|
149
|
+
* `spending_limits.alert_threshold_bps`.
|
|
150
|
+
*/
|
|
151
|
+
/* -------------------------------------------------------------------------- */
|
|
152
|
+
/* Invoices */
|
|
153
|
+
/* -------------------------------------------------------------------------- */
|
|
154
|
+
exports.BILLING_INVOICE_STATUSES = ['draft', 'open', 'paid', 'void'];
|
|
155
|
+
exports.billingInvoiceStatusSchema = zod_1.z.enum(exports.BILLING_INVOICE_STATUSES);
|
|
156
|
+
/**
|
|
157
|
+
* A period's charges, aggregated — the invoiced-enterprise settlement document.
|
|
158
|
+
*
|
|
159
|
+
* `subtotalAmount` is the EXACT sum of the receipts on the invoice, at full
|
|
160
|
+
* scale; `totalAmount` is the rounded figure actually charged. The difference is
|
|
161
|
+
* booked as an `invoice_rounding` ledger entry rather than discarded, because a
|
|
162
|
+
* discarded remainder is money that exists in one system and not the other.
|
|
163
|
+
*
|
|
164
|
+
* `minorUnitExponent` is carried rather than derived from the currency code:
|
|
165
|
+
* USD is 2, JPY is 0, BHD is 3, and that is not a property this platform's
|
|
166
|
+
* database knows. Storing what was used keeps the invoice reproducible.
|
|
167
|
+
*/
|
|
168
|
+
exports.billingInvoiceSchema = zod_1.z
|
|
169
|
+
.object({
|
|
170
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
171
|
+
schemaVersion: zod_1.z.literal(1),
|
|
172
|
+
id: zod_1.z.string().min(1).max(64),
|
|
173
|
+
accountId: identifiers_1.oxyAccountIdSchema,
|
|
174
|
+
currency: money_1.currencyCodeSchema,
|
|
175
|
+
periodStart: zod_1.z.string().datetime(),
|
|
176
|
+
periodEnd: zod_1.z.string().datetime(),
|
|
177
|
+
status: exports.billingInvoiceStatusSchema,
|
|
178
|
+
subtotalAmount: money_1.exactDecimalSchema,
|
|
179
|
+
totalAmount: money_1.exactDecimalSchema,
|
|
180
|
+
minorUnitExponent: zod_1.z.number().int().min(0).max(4),
|
|
181
|
+
/** The processor's own invoice id, for reconciliation. Never an authority. */
|
|
182
|
+
externalInvoiceRef: zod_1.z.string().min(1).max(255).optional(),
|
|
183
|
+
issuedAt: zod_1.z.string().datetime().optional(),
|
|
184
|
+
paidAt: zod_1.z.string().datetime().optional(),
|
|
185
|
+
receiptCount: zod_1.z.number().int().nonnegative().safe(),
|
|
186
|
+
})
|
|
187
|
+
.strict();
|
|
188
|
+
/* -------------------------------------------------------------------------- */
|
|
189
|
+
/* Payment processor boundary */
|
|
190
|
+
/* -------------------------------------------------------------------------- */
|
|
191
|
+
/** The processors this platform records payments from. */
|
|
192
|
+
exports.EXTERNAL_PAYMENT_PROVIDERS = ['stripe'];
|
|
193
|
+
exports.externalPaymentProviderSchema = zod_1.z.enum(exports.EXTERNAL_PAYMENT_PROVIDERS);
|
|
194
|
+
/**
|
|
195
|
+
* What kind of processor record `externalRef` names.
|
|
196
|
+
*
|
|
197
|
+
* Kept explicit because reconciliation compares like with like: a payment intent
|
|
198
|
+
* and the invoice it paid are two records of one movement of money, and counting
|
|
199
|
+
* both would double the external total.
|
|
200
|
+
*/
|
|
201
|
+
exports.EXTERNAL_PAYMENT_KINDS = ['payment_intent', 'invoice'];
|
|
202
|
+
exports.externalPaymentKindSchema = zod_1.z.enum(exports.EXTERNAL_PAYMENT_KINDS);
|
|
203
|
+
/**
|
|
204
|
+
* A processor payment that funded an Oxy balance.
|
|
205
|
+
*
|
|
206
|
+
* This is the reconciliation ANCHOR: one row per charge that landed in the
|
|
207
|
+
* ledger, carrying both the processor's reference and the ledger entry it
|
|
208
|
+
* produced. Without it, matching a Stripe charge back to a ledger entry means
|
|
209
|
+
* parsing an idempotency key, and a parser is not a foreign key.
|
|
210
|
+
*/
|
|
211
|
+
exports.externalPaymentSchema = zod_1.z
|
|
212
|
+
.object({
|
|
213
|
+
id: zod_1.z.string().min(1).max(64),
|
|
214
|
+
accountId: identifiers_1.oxyAccountIdSchema,
|
|
215
|
+
provider: exports.externalPaymentProviderSchema,
|
|
216
|
+
externalKind: exports.externalPaymentKindSchema,
|
|
217
|
+
externalRef: zod_1.z.string().min(1).max(255),
|
|
218
|
+
amount: money_1.exactDecimalSchema,
|
|
219
|
+
currency: money_1.currencyCodeSchema,
|
|
220
|
+
ledgerEntryId: zod_1.z.string().min(1).max(64),
|
|
221
|
+
occurredAt: zod_1.z.string().datetime(),
|
|
222
|
+
createdAt: zod_1.z.string().datetime(),
|
|
223
|
+
})
|
|
224
|
+
.strict();
|
|
225
|
+
/** Lifecycle of one automatic top-up. */
|
|
226
|
+
exports.AUTO_RECHARGE_STATUSES = ['pending', 'succeeded', 'failed'];
|
|
227
|
+
exports.autoRechargeStatusSchema = zod_1.z.enum(exports.AUTO_RECHARGE_STATUSES);
|
|
228
|
+
/**
|
|
229
|
+
* One attempt to top an account up automatically.
|
|
230
|
+
*
|
|
231
|
+
* Recorded BEFORE the processor is called and keyed on the account, currency and
|
|
232
|
+
* the window it fired in, so a sweep that runs twice — or two instances of it —
|
|
233
|
+
* charges a customer's card once. An off-session charge is the one operation on
|
|
234
|
+
* this surface where a duplicate is not merely a wrong number in a report.
|
|
235
|
+
*/
|
|
236
|
+
exports.autoRechargeAttemptSchema = zod_1.z
|
|
237
|
+
.object({
|
|
238
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
239
|
+
schemaVersion: zod_1.z.literal(1),
|
|
240
|
+
id: zod_1.z.string().min(1).max(64),
|
|
241
|
+
accountId: identifiers_1.oxyAccountIdSchema,
|
|
242
|
+
currency: money_1.currencyCodeSchema,
|
|
243
|
+
requestedAmount: money_1.exactDecimalSchema,
|
|
244
|
+
/** The spendable balance that triggered it — why this attempt exists. */
|
|
245
|
+
balanceAtTrigger: money_1.exactDecimalSchema,
|
|
246
|
+
status: exports.autoRechargeStatusSchema,
|
|
247
|
+
externalRef: zod_1.z.string().min(1).max(255).optional(),
|
|
248
|
+
/** The processor's own decline code. Never a free-form message. */
|
|
249
|
+
failureCode: zod_1.z.string().min(1).max(64).optional(),
|
|
250
|
+
createdAt: zod_1.z.string().datetime(),
|
|
251
|
+
updatedAt: zod_1.z.string().datetime(),
|
|
252
|
+
})
|
|
253
|
+
.strict();
|
|
254
|
+
/* -------------------------------------------------------------------------- */
|
|
255
|
+
/* Reconciliation */
|
|
256
|
+
/* -------------------------------------------------------------------------- */
|
|
257
|
+
/**
|
|
258
|
+
* How an Oxy record and a processor record fail to agree.
|
|
259
|
+
*
|
|
260
|
+
* A closed set, and every member is a DIFFERENT operational problem:
|
|
261
|
+
*
|
|
262
|
+
* - `missing_in_ledger` — the processor took money Oxy never credited. The
|
|
263
|
+
* customer paid and has no balance. This is the one that costs a customer.
|
|
264
|
+
* - `missing_in_external` — Oxy credited a balance with no processor charge
|
|
265
|
+
* behind it. This is the one that costs Oxy.
|
|
266
|
+
* - `amount_mismatch` — both exist and disagree.
|
|
267
|
+
* - `account_unresolved` — a processor charge whose customer maps to no Oxy
|
|
268
|
+
* account. Money arrived and nobody owns it.
|
|
269
|
+
*
|
|
270
|
+
* Collapsing them into a single "discrepancy" count is what makes a
|
|
271
|
+
* reconciliation report a number nobody acts on.
|
|
272
|
+
*/
|
|
273
|
+
exports.RECONCILIATION_DISCREPANCY_KINDS = [
|
|
274
|
+
'missing_in_ledger',
|
|
275
|
+
'missing_in_external',
|
|
276
|
+
'amount_mismatch',
|
|
277
|
+
'account_unresolved',
|
|
278
|
+
];
|
|
279
|
+
exports.reconciliationDiscrepancyKindSchema = zod_1.z.enum(exports.RECONCILIATION_DISCREPANCY_KINDS);
|
|
280
|
+
exports.RECONCILIATION_RUN_STATUSES = ['running', 'completed', 'failed'];
|
|
281
|
+
exports.reconciliationRunStatusSchema = zod_1.z.enum(exports.RECONCILIATION_RUN_STATUSES);
|
|
282
|
+
exports.reconciliationDiscrepancySchema = zod_1.z
|
|
283
|
+
.object({
|
|
284
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
285
|
+
schemaVersion: zod_1.z.literal(1),
|
|
286
|
+
id: zod_1.z.string().min(1).max(64),
|
|
287
|
+
runId: zod_1.z.string().min(1).max(64),
|
|
288
|
+
kind: exports.reconciliationDiscrepancyKindSchema,
|
|
289
|
+
accountId: identifiers_1.oxyAccountIdSchema.optional(),
|
|
290
|
+
externalRef: zod_1.z.string().min(1).max(255).optional(),
|
|
291
|
+
ledgerEntryId: zod_1.z.string().min(1).max(64).optional(),
|
|
292
|
+
/** What Oxy recorded. Absent for `missing_in_ledger`. */
|
|
293
|
+
ledgerAmount: money_1.exactDecimalSchema.optional(),
|
|
294
|
+
/** What the processor recorded. Absent for `missing_in_external`. */
|
|
295
|
+
externalAmount: money_1.exactDecimalSchema.optional(),
|
|
296
|
+
currency: money_1.currencyCodeSchema,
|
|
297
|
+
createdAt: zod_1.z.string().datetime(),
|
|
298
|
+
})
|
|
299
|
+
.strict();
|
|
300
|
+
/**
|
|
301
|
+
* One reconciliation pass over a window.
|
|
302
|
+
*
|
|
303
|
+
* The totals are carried beside the discrepancy list on purpose: two totals that
|
|
304
|
+
* agree while the lists differ is a real and common state (a charge recorded
|
|
305
|
+
* against the wrong account), and a report that only published a difference
|
|
306
|
+
* would call it clean.
|
|
307
|
+
*/
|
|
308
|
+
exports.reconciliationRunSchema = zod_1.z
|
|
309
|
+
.object({
|
|
310
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
311
|
+
schemaVersion: zod_1.z.literal(1),
|
|
312
|
+
id: zod_1.z.string().min(1).max(64),
|
|
313
|
+
provider: exports.externalPaymentProviderSchema,
|
|
314
|
+
/** Absent for a platform-wide pass. */
|
|
315
|
+
accountId: identifiers_1.oxyAccountIdSchema.optional(),
|
|
316
|
+
currency: money_1.currencyCodeSchema,
|
|
317
|
+
periodStart: zod_1.z.string().datetime(),
|
|
318
|
+
periodEnd: zod_1.z.string().datetime(),
|
|
319
|
+
status: exports.reconciliationRunStatusSchema,
|
|
320
|
+
ledgerTotal: money_1.exactDecimalSchema,
|
|
321
|
+
externalTotal: money_1.exactDecimalSchema,
|
|
322
|
+
discrepancyCount: zod_1.z.number().int().nonnegative().safe(),
|
|
323
|
+
startedAt: zod_1.z.string().datetime(),
|
|
324
|
+
completedAt: zod_1.z.string().datetime().optional(),
|
|
325
|
+
})
|
|
326
|
+
.strict();
|
|
327
|
+
exports.reconciliationReportSchema = zod_1.z
|
|
328
|
+
.object({
|
|
329
|
+
/** See `version.ts`: this shape is served to Console and to Alia. */
|
|
330
|
+
schemaVersion: zod_1.z.literal(1),
|
|
331
|
+
run: exports.reconciliationRunSchema,
|
|
332
|
+
discrepancies: zod_1.z.array(exports.reconciliationDiscrepancySchema),
|
|
333
|
+
})
|
|
334
|
+
.strict();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The signed Alia model release manifest — the ingestion contract for a
|
|
4
|
+
* first-party model release.
|
|
5
|
+
*
|
|
6
|
+
* The catalogue already STORES everything such a manifest carries: the model
|
|
7
|
+
* card, the licence block, the provenance and base model, the evaluation table,
|
|
8
|
+
* the safety metadata, and an artifact digest with a `sha256:<64 hex>` CHECK.
|
|
9
|
+
* What did not exist was the manifest itself — a single document Alia SIGNS,
|
|
10
|
+
* asserting all of it at once — and that is the gap this shape closes. Nothing
|
|
11
|
+
* here re-declares a catalogue field; the manifest COMPOSES the published shapes
|
|
12
|
+
* so a manifest and the catalogue row it produces cannot describe a release
|
|
13
|
+
* differently.
|
|
14
|
+
*
|
|
15
|
+
* ## The manifest tightens the revision it carries
|
|
16
|
+
*
|
|
17
|
+
* `modelRevisionSchema` makes `modelCardUrl`, `artifactDigest`, `evaluations`
|
|
18
|
+
* and `safety` optional, because a third-party route legitimately has none of
|
|
19
|
+
* them — Oxy did not train those weights and cannot publish a card for them. A
|
|
20
|
+
* FIRST-PARTY release has no such excuse: the documentation trail is the reason
|
|
21
|
+
* a release manifest exists at all, and a model Alia ships without one is not a
|
|
22
|
+
* release, it is a deployment. So the refinement below requires all four,
|
|
23
|
+
* without changing the catalogue shape that a third-party entry still parses
|
|
24
|
+
* through.
|
|
25
|
+
*
|
|
26
|
+
* ## `.strict()` at the top level, and here that is forced rather than chosen
|
|
27
|
+
*
|
|
28
|
+
* The shapes exchanged with the data plane tolerate an unknown field, because
|
|
29
|
+
* refusing a producer one minor version ahead is a worse failure than ignoring
|
|
30
|
+
* its addition (`version.ts`). A SIGNED document inverts that: the signature is
|
|
31
|
+
* over the canonical bytes of the manifest, so a field silently stripped at this
|
|
32
|
+
* parse is a field missing from the bytes Oxy re-canonicalizes, and verification
|
|
33
|
+
* fails. A tolerant parse would therefore report "the signature is invalid" for
|
|
34
|
+
* what is really "this build does not understand this manifest" — the wrong
|
|
35
|
+
* diagnosis of the right problem. Strict says the true thing, and the cost is
|
|
36
|
+
* bounded: ingestion is a release-time operation an operator retries once Oxy
|
|
37
|
+
* takes the newer contract, not a served request that becomes unsettleable.
|
|
38
|
+
*
|
|
39
|
+
* ## The ingestion path, which this file used to say did not exist
|
|
40
|
+
*
|
|
41
|
+
* It does now: `POST /inference/admin/model-releases`, defined by
|
|
42
|
+
* `modelReleaseIngestionRequestSchema` in `modelDocumentation.ts`. This shape is
|
|
43
|
+
* unchanged — the request COMPOSES it, alongside two records that are Oxy's own
|
|
44
|
+
* rather than the signer's (the GPAI documentation and the capability sheet a
|
|
45
|
+
* manifest does not carry), precisely so the bytes a signature covers stay
|
|
46
|
+
* exactly the bytes described here.
|
|
47
|
+
*
|
|
48
|
+
* The earlier objection was that a staff write path into an empty catalogue is
|
|
49
|
+
* an unexercised hazard. What answers it is containment rather than emptiness: an
|
|
50
|
+
* ingested revision lands with `is_current = false` and no deployment, so nothing
|
|
51
|
+
* it creates is servable or listed, and a route still needs an approved
|
|
52
|
+
* contract/legal review before any customer can select it.
|
|
53
|
+
*
|
|
54
|
+
* ## What is deliberately NOT here
|
|
55
|
+
*
|
|
56
|
+
* **No `payloadDigest` field.** The signature is over the canonical
|
|
57
|
+
* serialization of this manifest with `signatures` removed, and a verifier
|
|
58
|
+
* recomputes it. Storing the digest beside the document it digests would be a
|
|
59
|
+
* second source of truth for one fact, and a verifier that compared the
|
|
60
|
+
* signature against the DECLARED digest rather than the recomputed one would
|
|
61
|
+
* verify nothing at all.
|
|
62
|
+
*
|
|
63
|
+
* **No verification RESULT.** Whether a signature checked out is Oxy's finding
|
|
64
|
+
* about the document, not a claim the document makes about itself; a `verified`
|
|
65
|
+
* field inside a signed manifest is the signer asserting its own signature.
|
|
66
|
+
*
|
|
67
|
+
* ## The open owner decision this shape does not take
|
|
68
|
+
*
|
|
69
|
+
* **What signs, and what verifies, is not decided.** Oxy holds no Alia signing
|
|
70
|
+
* key, and whether to resolve `keyId` through the existing attestation machinery
|
|
71
|
+
* (`services/oxyVerificationResolver.ts`, the civic attestation code) or to
|
|
72
|
+
* introduce a dedicated Alia release key is a real choice with different
|
|
73
|
+
* custody, rotation and revocation consequences. So `keyId` is an OPAQUE
|
|
74
|
+
* identifier and this file names no registry that resolves it: either answer
|
|
75
|
+
* fits, and neither is presupposed. Until it is answered a manifest can be
|
|
76
|
+
* parsed and cannot be VERIFIED, so the ingestion path records no verification
|
|
77
|
+
* finding at all: it stores the signatures and the manifest as received, and the
|
|
78
|
+
* authority for the ingest is the staff member who performed it. A nullable
|
|
79
|
+
* `verified` column nothing ever writes would read, to whoever scanned the table
|
|
80
|
+
* later, as a check that ran.
|
|
81
|
+
*
|
|
82
|
+
* Decided in: docs/adr/0008-catalogue-concept-separation.md,
|
|
83
|
+
* docs/adr/0017-authorized-routes-in-the-envelope.md, issue #972 §12.
|
|
84
|
+
*/
|
|
85
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86
|
+
exports.aliaModelReleaseManifestSchema = exports.aliaReleaseSignatureSchema = exports.aliaReleaseArtifactSchema = void 0;
|
|
87
|
+
const zod_1 = require("zod");
|
|
88
|
+
const catalogue_1 = require("./catalogue");
|
|
89
|
+
const identifiers_1 = require("./identifiers");
|
|
90
|
+
/**
|
|
91
|
+
* One artifact of a release, by path and digest.
|
|
92
|
+
*
|
|
93
|
+
* `sizeBytes` is required beside the digest so a verifier can refuse a stream
|
|
94
|
+
* that is the wrong length before reading it to the end, rather than only after.
|
|
95
|
+
*/
|
|
96
|
+
exports.aliaReleaseArtifactSchema = zod_1.z
|
|
97
|
+
.object({
|
|
98
|
+
/** Path within the release, e.g. `model-00001-of-00004.safetensors`. */
|
|
99
|
+
path: zod_1.z.string().min(1).max(512),
|
|
100
|
+
digest: identifiers_1.sha256DigestSchema,
|
|
101
|
+
sizeBytes: zod_1.z.number().int().positive().safe(),
|
|
102
|
+
mediaType: zod_1.z.string().min(1).max(255).optional(),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
/**
|
|
106
|
+
* One detached signature over the manifest.
|
|
107
|
+
*
|
|
108
|
+
* `algorithm` is a CLOSED enum with one member, and both halves of that are
|
|
109
|
+
* deliberate. Closed, because a verifier that trusts a document's own algorithm
|
|
110
|
+
* name accepts whatever that document nominates, `none` included. One member,
|
|
111
|
+
* because Ed25519 is the scheme ADR 0012 already chose for asymmetric
|
|
112
|
+
* verification on this platform, and naming a scheme nothing here can check
|
|
113
|
+
* would be advertising a capability that does not exist. A second member lands
|
|
114
|
+
* when a verifier for it does — which is a closed enum gaining a member, and
|
|
115
|
+
* therefore a MINOR contract-set change the handshake surfaces (`version.ts`).
|
|
116
|
+
*
|
|
117
|
+
* `keyId` is opaque on purpose: see the header. It identifies the public key
|
|
118
|
+
* without saying what resolves it.
|
|
119
|
+
*
|
|
120
|
+
* The signature covers the canonical serialization (RFC 8785 JCS) of the
|
|
121
|
+
* manifest with `signatures` removed. The canonicalization is NAMED rather than
|
|
122
|
+
* left implicit because a digest over "the manifest" is not verifiable by two
|
|
123
|
+
* implementations that serialize JSON differently; naming it is a mechanical
|
|
124
|
+
* necessity and is independent of the open question of which key signs.
|
|
125
|
+
*/
|
|
126
|
+
exports.aliaReleaseSignatureSchema = zod_1.z
|
|
127
|
+
.object({
|
|
128
|
+
algorithm: zod_1.z.enum(['ed25519']),
|
|
129
|
+
canonicalization: zod_1.z.enum(['jcs']),
|
|
130
|
+
/** Opaque identifier of the public key. Resolving it is undecided. */
|
|
131
|
+
keyId: zod_1.z.string().min(1).max(256),
|
|
132
|
+
/**
|
|
133
|
+
* Unpadded base64url. Exactly 86 characters, which is a 64-byte Ed25519
|
|
134
|
+
* signature — the one algorithm above. A second algorithm moves this length
|
|
135
|
+
* into a refinement keyed on `algorithm`.
|
|
136
|
+
*/
|
|
137
|
+
signature: zod_1.z
|
|
138
|
+
.string()
|
|
139
|
+
.regex(/^[A-Za-z0-9_-]{86}$/, 'signature must be a 64-byte ed25519 signature in unpadded base64url'),
|
|
140
|
+
signedAt: identifiers_1.inferenceTimestampSchema,
|
|
141
|
+
})
|
|
142
|
+
.strict();
|
|
143
|
+
/**
|
|
144
|
+
* A signed release of an `alia/*` model revision.
|
|
145
|
+
*
|
|
146
|
+
* `signatures` is a LIST rather than one signature, because "what signs" is
|
|
147
|
+
* undecided: a single field would presuppose one signer, while a list lets an
|
|
148
|
+
* Alia release key and an existing attestation co-sign the same document without
|
|
149
|
+
* either being retrofitted later.
|
|
150
|
+
*/
|
|
151
|
+
exports.aliaModelReleaseManifestSchema = zod_1.z
|
|
152
|
+
.object({
|
|
153
|
+
/** See `version.ts`: an ingestion payload is a whole message on the wire. */
|
|
154
|
+
schemaVersion: zod_1.z.literal(1),
|
|
155
|
+
/** The release's own identity, so ingestion is idempotent on it. */
|
|
156
|
+
releaseId: zod_1.z.string().min(1).max(128),
|
|
157
|
+
issuedAt: identifiers_1.inferenceTimestampSchema,
|
|
158
|
+
/**
|
|
159
|
+
* The revision being released. Carries its OWN `schemaVersion`, like
|
|
160
|
+
* `billingProfileSchema` inside `accountBillingStateSchema`: the manifest's
|
|
161
|
+
* version governs the manifest and the revision's governs the revision,
|
|
162
|
+
* which is two versions of two things rather than two versions of one.
|
|
163
|
+
*/
|
|
164
|
+
revision: catalogue_1.modelRevisionSchema,
|
|
165
|
+
/** On the MODEL rather than the revision in the catalogue, so carried here. */
|
|
166
|
+
provenance: catalogue_1.modelProvenanceSchema,
|
|
167
|
+
license: catalogue_1.modelLicenseSchema,
|
|
168
|
+
artifacts: zod_1.z.array(exports.aliaReleaseArtifactSchema).min(1),
|
|
169
|
+
signatures: zod_1.z.array(exports.aliaReleaseSignatureSchema).min(1),
|
|
170
|
+
})
|
|
171
|
+
.strict()
|
|
172
|
+
.superRefine((manifest, ctx) => {
|
|
173
|
+
// The same rule `catalogueModelSchema` enforces on a model, applied to the
|
|
174
|
+
// carrier that creates one: `alia/*` names models Alia actually owns or
|
|
175
|
+
// derived, and a manifest is the document that would launder somebody else's
|
|
176
|
+
// weights into the namespace.
|
|
177
|
+
const publisher = manifest.revision.modelId.slice(0, manifest.revision.modelId.indexOf('/'));
|
|
178
|
+
if (publisher !== identifiers_1.RESERVED_ALIA_PUBLISHER) {
|
|
179
|
+
ctx.addIssue({
|
|
180
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
181
|
+
path: ['revision', 'modelId'],
|
|
182
|
+
message: `an Alia release manifest releases a ${identifiers_1.RESERVED_ALIA_PUBLISHER}/* model`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (manifest.provenance.releaseKind !== 'first_party_original' &&
|
|
186
|
+
manifest.provenance.releaseKind !== 'first_party_derived') {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
189
|
+
path: ['provenance', 'releaseKind'],
|
|
190
|
+
message: 'an Alia release manifest describes a first-party release',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// A derived model's base is the licence-attribution trail. Recording the
|
|
194
|
+
// derivation without naming what it derives from loses exactly the fact
|
|
195
|
+
// attribution needs.
|
|
196
|
+
if (manifest.provenance.releaseKind === 'first_party_derived' &&
|
|
197
|
+
manifest.provenance.baseModelId === undefined) {
|
|
198
|
+
ctx.addIssue({
|
|
199
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
200
|
+
path: ['provenance', 'baseModelId'],
|
|
201
|
+
message: 'a derived release names the model it derives from',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// The four fields a third-party catalogue entry may omit and a first-party
|
|
205
|
+
// release may not. See the header.
|
|
206
|
+
if (manifest.revision.modelCardUrl === undefined) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
209
|
+
path: ['revision', 'modelCardUrl'],
|
|
210
|
+
message: 'a first-party release publishes a model card',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (manifest.revision.safety === undefined) {
|
|
214
|
+
ctx.addIssue({
|
|
215
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
216
|
+
path: ['revision', 'safety'],
|
|
217
|
+
message: 'a first-party release publishes its safety metadata',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (manifest.revision.evaluations.length === 0) {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
223
|
+
path: ['revision', 'evaluations'],
|
|
224
|
+
message: 'a first-party release publishes at least one evaluation result',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// The digest the catalogue will serve has to be one of the digests this
|
|
228
|
+
// manifest signed. Otherwise the signature covers a set of artifacts that
|
|
229
|
+
// does not include the weights anybody runs.
|
|
230
|
+
if (manifest.revision.artifactDigest === undefined) {
|
|
231
|
+
ctx.addIssue({
|
|
232
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
233
|
+
path: ['revision', 'artifactDigest'],
|
|
234
|
+
message: 'a first-party release names the digest of the artifact it serves',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else if (!manifest.artifacts.some((artifact) => artifact.digest === manifest.revision.artifactDigest)) {
|
|
238
|
+
ctx.addIssue({
|
|
239
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
240
|
+
path: ['revision', 'artifactDigest'],
|
|
241
|
+
message: 'the served artifact digest must appear among the signed artifacts',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const paths = manifest.artifacts.map((artifact) => artifact.path);
|
|
245
|
+
if (new Set(paths).size !== paths.length) {
|
|
246
|
+
ctx.addIssue({
|
|
247
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
248
|
+
path: ['artifacts'],
|
|
249
|
+
message: 'each artifact path appears once in a release',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Two signatures from one key are one signature written twice, and a
|
|
253
|
+
// duplicate would make a "two independent signers" check pass on one signer.
|
|
254
|
+
const keyIds = manifest.signatures.map((signature) => signature.keyId);
|
|
255
|
+
if (new Set(keyIds).size !== keyIds.length) {
|
|
256
|
+
ctx.addIssue({
|
|
257
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
258
|
+
path: ['signatures'],
|
|
259
|
+
message: 'each signing key signs a manifest once',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|