@juspay/neurolink 11.29.2 → 11.30.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 +3 -3
- package/dist/auth/anthropicOAuth.d.ts +50 -0
- package/dist/auth/anthropicOAuth.js +78 -0
- package/dist/browser/neurolink.min.js +393 -393
- package/dist/cli/commands/proxy.d.ts +2 -0
- package/dist/cli/commands/proxy.js +284 -4
- package/dist/cli/commands/proxyExpose.d.ts +35 -0
- package/dist/cli/commands/proxyExpose.js +252 -0
- package/dist/cli/commands/proxyPeer.d.ts +29 -0
- package/dist/cli/commands/proxyPeer.js +738 -0
- package/dist/cli/commands/proxyShare.d.ts +37 -0
- package/dist/cli/commands/proxyShare.js +1080 -0
- package/dist/cli/parser.js +7 -1
- package/dist/proxy/peerStore.d.ts +52 -0
- package/dist/proxy/peerStore.js +324 -0
- package/dist/proxy/peerTransport.d.ts +38 -0
- package/dist/proxy/peerTransport.js +242 -0
- package/dist/proxy/proxyPaths.d.ts +8 -0
- package/dist/proxy/proxyPaths.js +55 -17
- package/dist/proxy/requestLogger.js +8 -0
- package/dist/proxy/residentGrants.d.ts +57 -0
- package/dist/proxy/residentGrants.js +393 -0
- package/dist/proxy/shareAudit.d.ts +81 -0
- package/dist/proxy/shareAudit.js +280 -0
- package/dist/proxy/shareContext.d.ts +38 -0
- package/dist/proxy/shareContext.js +92 -0
- package/dist/proxy/shareGate.d.ts +64 -0
- package/dist/proxy/shareGate.js +216 -0
- package/dist/proxy/shareGrants.d.ts +115 -0
- package/dist/proxy/shareGrants.js +590 -0
- package/dist/proxy/shareLease.d.ts +101 -0
- package/dist/proxy/shareLease.js +192 -0
- package/dist/proxy/shareLedger.d.ts +105 -0
- package/dist/proxy/shareLedger.js +406 -0
- package/dist/proxy/shareListener.d.ts +60 -0
- package/dist/proxy/shareListener.js +143 -0
- package/dist/proxy/shareNotes.d.ts +97 -0
- package/dist/proxy/shareNotes.js +234 -0
- package/dist/proxy/sharePolicy.d.ts +110 -0
- package/dist/proxy/sharePolicy.js +366 -0
- package/dist/proxy/shareProvisioning.d.ts +110 -0
- package/dist/proxy/shareProvisioning.js +237 -0
- package/dist/proxy/shareReceipts.d.ts +99 -0
- package/dist/proxy/shareReceipts.js +303 -0
- package/dist/proxy/shareSigning.d.ts +40 -0
- package/dist/proxy/shareSigning.js +78 -0
- package/dist/server/routes/claudeProxyRoutes.js +1066 -3
- package/dist/types/cli.d.ts +61 -0
- package/dist/types/proxy.d.ts +781 -0
- package/package.json +2 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peer-sharing admission policy.
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation, no I/O — every input is passed in, so this module is cheap on
|
|
5
|
+
* the hot path and directly exercisable from a test.
|
|
6
|
+
*
|
|
7
|
+
* The gate set is deliberately **orthogonal and AND-ed**: a grant may carry a
|
|
8
|
+
* headroom floor *and* a window-slice ceiling *and* a model allowlist, and each
|
|
9
|
+
* is checked independently. The effective allowance is therefore the minimum
|
|
10
|
+
* across whatever is configured, which is what lets "share only what I am not
|
|
11
|
+
* using, and never more than a fifth of it" be one grant rather than two
|
|
12
|
+
* competing modes.
|
|
13
|
+
*
|
|
14
|
+
* Evaluation splits in two, because the two halves answer different questions:
|
|
15
|
+
*
|
|
16
|
+
* - `evaluateShareAdmission` — request-level. Is this borrower allowed to ask at
|
|
17
|
+
* all, right now, for this model?
|
|
18
|
+
* - `filterAccountsForGrant` — account-level. Of the lender's accounts, which
|
|
19
|
+
* may serve this borrower? Headroom and slice ceilings live here because they
|
|
20
|
+
* are properties of an individual account's windows, not of the request.
|
|
21
|
+
*
|
|
22
|
+
* @module proxy/sharePolicy
|
|
23
|
+
*/
|
|
24
|
+
const HOUR_MS = 3_600_000;
|
|
25
|
+
/** Human-facing text for a refusal. Kept terse — it reaches the borrower. */
|
|
26
|
+
const REFUSAL_MESSAGES = {
|
|
27
|
+
missing_token: "No share token was presented.",
|
|
28
|
+
unknown_token: "Share token is not recognized.",
|
|
29
|
+
malformed_token: "Share token is malformed.",
|
|
30
|
+
paused: "The lender has paused this share.",
|
|
31
|
+
revoked: "This share has been revoked.",
|
|
32
|
+
expired: "This share has expired.",
|
|
33
|
+
out_of_window: "This share is outside its allowed hours.",
|
|
34
|
+
model_not_allowed: "This share does not cover the requested model.",
|
|
35
|
+
exhausted: "This share has no credit remaining.",
|
|
36
|
+
rate_limited: "This share's request rate limit was exceeded.",
|
|
37
|
+
concurrency_limited: "This share's concurrent request limit was reached.",
|
|
38
|
+
reserve_floor: "The lender's reserved headroom is in force.",
|
|
39
|
+
spillover_inactive: "This share serves only the lender's spare capacity, and none is spare yet.",
|
|
40
|
+
slice_exhausted: "This share has used its allotted portion of the window.",
|
|
41
|
+
no_capacity: "No lender account can currently serve this share.",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Narrow an admission to its refusing half.
|
|
45
|
+
*
|
|
46
|
+
* A plain `!admission.admitted` check would do this under `strict`, but one of
|
|
47
|
+
* the package's build steps compiles without `strictNullChecks`, where TypeScript
|
|
48
|
+
* declines to narrow a boolean discriminant at all. An explicit predicate holds
|
|
49
|
+
* in both modes.
|
|
50
|
+
*/
|
|
51
|
+
export function isShareRefusal(admission) {
|
|
52
|
+
return !admission.admitted;
|
|
53
|
+
}
|
|
54
|
+
export function shareRefusalMessage(reason) {
|
|
55
|
+
return REFUSAL_MESSAGES[reason];
|
|
56
|
+
}
|
|
57
|
+
/** HTTP status for a refusal. 401 is "who are you", 403 is "not you, not ever
|
|
58
|
+
* under this grant", 429 is "not now" — the borrower retries only the last. */
|
|
59
|
+
export function shareRefusalStatus(reason) {
|
|
60
|
+
switch (reason) {
|
|
61
|
+
case "missing_token":
|
|
62
|
+
case "unknown_token":
|
|
63
|
+
case "malformed_token":
|
|
64
|
+
return 401;
|
|
65
|
+
case "paused":
|
|
66
|
+
case "revoked":
|
|
67
|
+
case "expired":
|
|
68
|
+
case "out_of_window":
|
|
69
|
+
case "model_not_allowed":
|
|
70
|
+
return 403;
|
|
71
|
+
default:
|
|
72
|
+
return 429;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function refuse(reason, options = {}) {
|
|
76
|
+
return {
|
|
77
|
+
admitted: false,
|
|
78
|
+
status: shareRefusalStatus(reason),
|
|
79
|
+
reason,
|
|
80
|
+
message: REFUSAL_MESSAGES[reason],
|
|
81
|
+
...(options.retryAfterSeconds !== undefined
|
|
82
|
+
? { retryAfterSeconds: options.retryAfterSeconds }
|
|
83
|
+
: {}),
|
|
84
|
+
...(options.grant ? { grant: options.grant } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Is `now` inside the grant's allowed hours?
|
|
89
|
+
*
|
|
90
|
+
* A window whose start hour is greater than its end hour wraps midnight
|
|
91
|
+
* (`21 → 9` means the night shift), which is the common case for lending
|
|
92
|
+
* capacity you are asleep through.
|
|
93
|
+
*/
|
|
94
|
+
export function isWithinSchedule(schedule, now) {
|
|
95
|
+
const hour = new Date(now).getHours();
|
|
96
|
+
const { fromHour, toHour } = schedule;
|
|
97
|
+
if (fromHour === toHour) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return fromHour < toHour
|
|
101
|
+
? hour >= fromHour && hour < toHour
|
|
102
|
+
: hour >= fromHour || hour < toHour;
|
|
103
|
+
}
|
|
104
|
+
/** Seconds until the schedule next opens, for an honest `Retry-After`. */
|
|
105
|
+
function secondsUntilScheduleOpens(schedule, now) {
|
|
106
|
+
const next = new Date(now);
|
|
107
|
+
next.setMinutes(0, 0, 0);
|
|
108
|
+
for (let ahead = 1; ahead <= 24; ahead += 1) {
|
|
109
|
+
next.setHours(next.getHours() + 1);
|
|
110
|
+
if (isWithinSchedule(schedule, next.getTime())) {
|
|
111
|
+
return Math.max(1, Math.round((next.getTime() - now) / 1000));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return 3600;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Does the requested model fall inside the allowlist?
|
|
118
|
+
*
|
|
119
|
+
* Entries are matched as case-insensitive substrings so a grant can name a tier
|
|
120
|
+
* (`sonnet`) rather than having to track every dated model id.
|
|
121
|
+
*/
|
|
122
|
+
export function isModelAllowed(models, model) {
|
|
123
|
+
if (!models || models.length === 0) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (!model) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const normalized = model.toLowerCase();
|
|
130
|
+
return models.some((entry) => normalized.includes(entry.toLowerCase()));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Request-level admission.
|
|
134
|
+
*
|
|
135
|
+
* Order matters: identity and lifecycle first (they are permanent refusals),
|
|
136
|
+
* then scope, then the transient limits that carry a `Retry-After`. A borrower
|
|
137
|
+
* that is told "paused" must not also be told "slow down".
|
|
138
|
+
*/
|
|
139
|
+
export function evaluateShareAdmission(input) {
|
|
140
|
+
const { grant, now, model, counters, coinBalance } = input;
|
|
141
|
+
if (grant.state !== "active") {
|
|
142
|
+
const reason = grant.state === "paused"
|
|
143
|
+
? "paused"
|
|
144
|
+
: grant.state === "revoked"
|
|
145
|
+
? "revoked"
|
|
146
|
+
: "expired";
|
|
147
|
+
return refuse(reason, { grant });
|
|
148
|
+
}
|
|
149
|
+
if (grant.gates.notAfter !== undefined && grant.gates.notAfter <= now) {
|
|
150
|
+
return refuse("expired", { grant });
|
|
151
|
+
}
|
|
152
|
+
if (grant.gates.schedule && !isWithinSchedule(grant.gates.schedule, now)) {
|
|
153
|
+
return refuse("out_of_window", {
|
|
154
|
+
grant,
|
|
155
|
+
retryAfterSeconds: secondsUntilScheduleOpens(grant.gates.schedule, now),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!isModelAllowed(grant.gates.models, model)) {
|
|
159
|
+
return refuse("model_not_allowed", { grant });
|
|
160
|
+
}
|
|
161
|
+
const rate = grant.gates.rate;
|
|
162
|
+
if (rate?.concurrency !== undefined &&
|
|
163
|
+
counters.inFlight >= rate.concurrency) {
|
|
164
|
+
return refuse("concurrency_limited", { grant, retryAfterSeconds: 1 });
|
|
165
|
+
}
|
|
166
|
+
if (rate?.perMinute !== undefined &&
|
|
167
|
+
counters.requestsInLastMinute >= rate.perMinute) {
|
|
168
|
+
return refuse("rate_limited", { grant, retryAfterSeconds: 60 });
|
|
169
|
+
}
|
|
170
|
+
if (grant.entitlement.ledger === "coins" &&
|
|
171
|
+
(coinBalance ?? grant.entitlement.coins ?? 0) <= 0) {
|
|
172
|
+
return refuse("exhausted", { grant });
|
|
173
|
+
}
|
|
174
|
+
return { admitted: true, grant };
|
|
175
|
+
}
|
|
176
|
+
/** The slice ceiling in force, taking spillover's own ceiling into account. */
|
|
177
|
+
function effectiveSlicePct(base, spilloverCap) {
|
|
178
|
+
const tighten = (configured) => {
|
|
179
|
+
if (configured === undefined) {
|
|
180
|
+
return spilloverCap;
|
|
181
|
+
}
|
|
182
|
+
if (spilloverCap === undefined) {
|
|
183
|
+
return configured;
|
|
184
|
+
}
|
|
185
|
+
return Math.min(configured, spilloverCap);
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
session: tighten(base?.session5hPct),
|
|
189
|
+
weekly: tighten(base?.weekly7dPct),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Is the account inside its spillover window — close enough to a reset, with
|
|
194
|
+
* little enough consumed, that the remaining capacity would otherwise expire?
|
|
195
|
+
*
|
|
196
|
+
* Unknown reset times fail closed. A spillover grant is a promise about capacity
|
|
197
|
+
* that is *about to be lost*; without a reset time there is no such promise to
|
|
198
|
+
* keep, and guessing would hand out capacity the lender still intends to use.
|
|
199
|
+
*/
|
|
200
|
+
export function isSpilloverActive(gates, account, now) {
|
|
201
|
+
const spillover = gates.spillover;
|
|
202
|
+
if (!spillover) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const windowMs = spillover.beforeResetHours * HOUR_MS;
|
|
206
|
+
const threshold = spillover.whenUtilizationBelowPct / 100;
|
|
207
|
+
const nearWeekly = account.weeklyResetAt !== null &&
|
|
208
|
+
account.weeklyResetAt - now <= windowMs &&
|
|
209
|
+
account.weeklyResetAt > now &&
|
|
210
|
+
account.weeklyUsed !== null &&
|
|
211
|
+
account.weeklyUsed < threshold;
|
|
212
|
+
const nearSession = account.sessionResetAt !== null &&
|
|
213
|
+
account.sessionResetAt - now <= windowMs &&
|
|
214
|
+
account.sessionResetAt > now &&
|
|
215
|
+
account.sessionUsed !== null &&
|
|
216
|
+
account.sessionUsed < threshold;
|
|
217
|
+
return nearWeekly || nearSession;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Split the pool into the accounts this grant may draw on and the rest.
|
|
221
|
+
*
|
|
222
|
+
* An entry in `gates.accounts` matches either the full store key
|
|
223
|
+
* (`anthropic:alice`) or the bare label (`alice`), because operators think in
|
|
224
|
+
* labels and the routing path thinks in keys.
|
|
225
|
+
*
|
|
226
|
+
* Exported because the scope decides more than admission: it is also the
|
|
227
|
+
* denominator of the pool-wide slice, and computing that over accounts the grant
|
|
228
|
+
* can never touch would loosen the ceiling in proportion to how many there are.
|
|
229
|
+
*/
|
|
230
|
+
export function accountsInGrantScope(gates, accounts) {
|
|
231
|
+
if (!gates.accounts || gates.accounts.length === 0) {
|
|
232
|
+
return { inScope: [...accounts], outOfScope: [] };
|
|
233
|
+
}
|
|
234
|
+
const allowedKeys = new Set(gates.accounts.map((entry) => entry.trim().toLowerCase()));
|
|
235
|
+
const inScope = [];
|
|
236
|
+
const outOfScope = [];
|
|
237
|
+
for (const account of accounts) {
|
|
238
|
+
const key = account.accountKey.toLowerCase();
|
|
239
|
+
const label = key.includes(":") ? key.slice(key.indexOf(":") + 1) : key;
|
|
240
|
+
if (allowedKeys.has(key) || allowedKeys.has(label)) {
|
|
241
|
+
inScope.push(account);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
outOfScope.push(account);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { inScope, outOfScope };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Decide which of the lender's accounts this grant may draw on.
|
|
251
|
+
*
|
|
252
|
+
* Returning exclusions alongside the survivors is deliberate: when nothing
|
|
253
|
+
* survives, the caller needs to say *why* — "your slice is spent" and "I am
|
|
254
|
+
* holding back my reserve" are different answers, and only one of them will
|
|
255
|
+
* change on its own.
|
|
256
|
+
*/
|
|
257
|
+
export function filterAccountsForGrant(grant, accounts, now,
|
|
258
|
+
/**
|
|
259
|
+
* Required, not optional. `gates.maxSlice` is a ceiling on what this grant
|
|
260
|
+
* has already drawn from the pool, and with nothing to compare against the
|
|
261
|
+
* only available answer is "not yet" — so an omitted argument would not relax
|
|
262
|
+
* the ceiling, it would remove it. `readSharePoolWindowUsage` returns zeroed
|
|
263
|
+
* fractions for a grant with no history, which is the honest empty value.
|
|
264
|
+
*/
|
|
265
|
+
poolUsage) {
|
|
266
|
+
const gates = grant.gates;
|
|
267
|
+
// Scope first. "The pool" a slice ceiling divides is the set of accounts this
|
|
268
|
+
// grant may draw on at all — not every credential the node happens to hold.
|
|
269
|
+
// Counting the others would inflate the denominator and loosen the ceiling by
|
|
270
|
+
// exactly the ratio between the two sets.
|
|
271
|
+
const { inScope, outOfScope } = accountsInGrantScope(gates, accounts);
|
|
272
|
+
// The pool ceiling is one decision about the whole pool, so it is settled
|
|
273
|
+
// before any account is considered. Spillover's tighter cap applies as soon
|
|
274
|
+
// as any in-scope account is spilling — the borrower is drawing on pooled
|
|
275
|
+
// capacity, not on one credential's clock.
|
|
276
|
+
const anySpillover = inScope.some((account) => isSpilloverActive(gates, account, now));
|
|
277
|
+
const poolSlice = effectiveSlicePct(gates.maxSlice, anySpillover ? gates.spillover?.maxSlicePct : undefined);
|
|
278
|
+
const poolSpent = (poolSlice.session !== undefined &&
|
|
279
|
+
poolUsage.sessionFraction * 100 >= poolSlice.session) ||
|
|
280
|
+
(poolSlice.weekly !== undefined &&
|
|
281
|
+
poolUsage.weeklyFraction * 100 >= poolSlice.weekly);
|
|
282
|
+
if (poolSpent) {
|
|
283
|
+
// Refused everywhere, including on idle accounts. That is what a pool
|
|
284
|
+
// ceiling means: the borrower has had its share of the whole.
|
|
285
|
+
return {
|
|
286
|
+
allowed: [],
|
|
287
|
+
excluded: [
|
|
288
|
+
...inScope.map((account) => ({
|
|
289
|
+
accountKey: account.accountKey,
|
|
290
|
+
reason: "slice_exhausted",
|
|
291
|
+
})),
|
|
292
|
+
...outOfScope.map((account) => ({
|
|
293
|
+
accountKey: account.accountKey,
|
|
294
|
+
reason: "no_capacity",
|
|
295
|
+
})),
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const allowed = [];
|
|
300
|
+
const excluded = outOfScope.map((account) => ({
|
|
301
|
+
accountKey: account.accountKey,
|
|
302
|
+
reason: "no_capacity",
|
|
303
|
+
}));
|
|
304
|
+
for (const account of inScope) {
|
|
305
|
+
const spilloverActive = isSpilloverActive(gates, account, now);
|
|
306
|
+
if (gates.spillover && !spilloverActive) {
|
|
307
|
+
// Not `reserve_floor`: no headroom is being held back, the lender simply
|
|
308
|
+
// has not used enough of this account for its spare capacity to exist
|
|
309
|
+
// yet. Both are transient, but they clear on opposite movements and the
|
|
310
|
+
// borrower is told which one it is waiting on.
|
|
311
|
+
excluded.push({
|
|
312
|
+
accountKey: account.accountKey,
|
|
313
|
+
reason: "spillover_inactive",
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const floor = gates.reserveFloor;
|
|
318
|
+
if (floor) {
|
|
319
|
+
const sessionBlocked = floor.session5hPct !== undefined &&
|
|
320
|
+
account.sessionUsed !== null &&
|
|
321
|
+
account.sessionUsed * 100 > 100 - floor.session5hPct;
|
|
322
|
+
const weeklyBlocked = floor.weekly7dPct !== undefined &&
|
|
323
|
+
account.weeklyUsed !== null &&
|
|
324
|
+
account.weeklyUsed * 100 > 100 - floor.weekly7dPct;
|
|
325
|
+
if (sessionBlocked || weeklyBlocked) {
|
|
326
|
+
excluded.push({
|
|
327
|
+
accountKey: account.accountKey,
|
|
328
|
+
reason: "reserve_floor",
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Per-account ceiling, opt-in and independent of the pool one above.
|
|
334
|
+
const slice = effectiveSlicePct(gates.maxSlicePerAccount, spilloverActive ? gates.spillover?.maxSlicePct : undefined);
|
|
335
|
+
const sessionSliceSpent = slice.session !== undefined &&
|
|
336
|
+
account.borrowedSessionFraction * 100 >= slice.session;
|
|
337
|
+
const weeklySliceSpent = slice.weekly !== undefined &&
|
|
338
|
+
account.borrowedWeeklyFraction * 100 >= slice.weekly;
|
|
339
|
+
if (sessionSliceSpent || weeklySliceSpent) {
|
|
340
|
+
excluded.push({
|
|
341
|
+
accountKey: account.accountKey,
|
|
342
|
+
reason: "slice_exhausted",
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
allowed.push(account.accountKey);
|
|
347
|
+
}
|
|
348
|
+
return { allowed, excluded };
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Collapse per-account exclusions into the single reason the borrower is told
|
|
352
|
+
* when nothing survived. A transient cause outranks a structural one so the
|
|
353
|
+
* borrower learns whether waiting is worth anything.
|
|
354
|
+
*/
|
|
355
|
+
export function summarizeAccountExclusions(excluded) {
|
|
356
|
+
if (excluded.some((entry) => entry.reason === "reserve_floor")) {
|
|
357
|
+
return "reserve_floor";
|
|
358
|
+
}
|
|
359
|
+
if (excluded.some((entry) => entry.reason === "spillover_inactive")) {
|
|
360
|
+
return "spillover_inactive";
|
|
361
|
+
}
|
|
362
|
+
if (excluded.some((entry) => entry.reason === "slice_exhausted")) {
|
|
363
|
+
return "slice_exhausted";
|
|
364
|
+
}
|
|
365
|
+
return "no_capacity";
|
|
366
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split-PKCE provisioning — minting a resident credential without ever holding
|
|
3
|
+
* one.
|
|
4
|
+
*
|
|
5
|
+
* The earlier design had the lender complete an authorization and hand the
|
|
6
|
+
* borrower a file containing a live access token and a refresh token. That file
|
|
7
|
+
* is copyable, re-sharable, and exists on disk in plaintext at both ends for as
|
|
8
|
+
* long as anyone forgets to delete it. Nothing about the lease constrains a
|
|
9
|
+
* credential someone else already copied.
|
|
10
|
+
*
|
|
11
|
+
* This splits the flow along the seam PKCE was designed for:
|
|
12
|
+
*
|
|
13
|
+
* 1. The **borrower** generates a verifier and sends only its SHA-256
|
|
14
|
+
* challenge, over its authenticated grant.
|
|
15
|
+
* 2. The **lender** authorizes in their own browser, against their own account,
|
|
16
|
+
* with the borrower's challenge in the URL.
|
|
17
|
+
* 3. The **authorization code** comes back to the lender and is relayed to the
|
|
18
|
+
* borrower.
|
|
19
|
+
* 4. The **borrower** exchanges code + verifier for tokens, on its own machine.
|
|
20
|
+
*
|
|
21
|
+
* The code is worthless to anyone who intercepts it: the token endpoint will not
|
|
22
|
+
* exchange it without the verifier, which never left the borrower. The lender
|
|
23
|
+
* never possesses a token for the credential it just authorized, so there is
|
|
24
|
+
* nothing for it to leak, re-send, or forget to delete.
|
|
25
|
+
*
|
|
26
|
+
* **What this module owns** is the lender's side of that conversation: the
|
|
27
|
+
* pending request, its binding to one grant, its expiry, and the rule that a
|
|
28
|
+
* code is handed over exactly once.
|
|
29
|
+
*
|
|
30
|
+
* @module proxy/shareProvisioning
|
|
31
|
+
*/
|
|
32
|
+
import type { ProxyShareProvisionOutcome, ProxyShareProvisionRequest } from "../types/index.js";
|
|
33
|
+
/**
|
|
34
|
+
* How long a challenge stays claimable.
|
|
35
|
+
*
|
|
36
|
+
* Long enough for a lender to notice a request and finish a browser login,
|
|
37
|
+
* short enough that a challenge left lying around is not a standing invitation.
|
|
38
|
+
*/
|
|
39
|
+
export declare const PROVISION_REQUEST_TTL_MS = 900000;
|
|
40
|
+
export declare function initShareProvisioning(filePath: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Narrow an open/authorize outcome to its failing half.
|
|
43
|
+
*
|
|
44
|
+
* One of the package's build steps compiles without `strictNullChecks`, where a
|
|
45
|
+
* boolean discriminant does not narrow. Callers reading `.reason` need this.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isProvisionFailure(outcome: ProxyShareProvisionOutcome): outcome is {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/** Is this request still live? Expiry and consumption both retire it. */
|
|
52
|
+
export declare function isProvisionRequestOpen(request: ProxyShareProvisionRequest, now?: number): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Record a borrower's challenge.
|
|
55
|
+
*
|
|
56
|
+
* One open request per grant: a second one replaces the first rather than
|
|
57
|
+
* queueing, because the borrower that just asked is the one waiting, and an
|
|
58
|
+
* abandoned challenge should not be claimable later by whoever finds it.
|
|
59
|
+
*
|
|
60
|
+
* Rejects anything that is not a well-formed S256 challenge. The lender is about
|
|
61
|
+
* to put this string into an authorization URL against its own account, so it
|
|
62
|
+
* is not the place to be relaxed about input.
|
|
63
|
+
*/
|
|
64
|
+
export declare function openProvisionRequest(args: {
|
|
65
|
+
grantId: string;
|
|
66
|
+
codeChallenge: string;
|
|
67
|
+
state: string;
|
|
68
|
+
now?: number;
|
|
69
|
+
}): Promise<ProxyShareProvisionOutcome>;
|
|
70
|
+
/** The open request for a grant, if there is one. */
|
|
71
|
+
export declare function getProvisionRequest(grantId: string, now?: number): Promise<ProxyShareProvisionRequest | undefined>;
|
|
72
|
+
/** Every open request, for a lender deciding which to authorize. */
|
|
73
|
+
export declare function listProvisionRequests(now?: number): Promise<ProxyShareProvisionRequest[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Attach the authorization code the lender's browser produced.
|
|
76
|
+
*
|
|
77
|
+
* The code is bound to the grant that asked for it and to the account the lender
|
|
78
|
+
* authorized against — the same account the drift audit will later reconcile —
|
|
79
|
+
* so a code cannot be redirected to a different grant after the fact.
|
|
80
|
+
*/
|
|
81
|
+
export declare function authorizeProvisionRequest(args: {
|
|
82
|
+
grantId: string;
|
|
83
|
+
code: string;
|
|
84
|
+
accountLabel?: string;
|
|
85
|
+
now?: number;
|
|
86
|
+
}): Promise<ProxyShareProvisionOutcome>;
|
|
87
|
+
/**
|
|
88
|
+
* Hand the code to the borrower — once.
|
|
89
|
+
*
|
|
90
|
+
* Single-use is the whole binding: an authorization code that could be claimed
|
|
91
|
+
* twice would let anyone who replayed the call mint a second credential on the
|
|
92
|
+
* lender's account. Consumption is recorded before the value is returned.
|
|
93
|
+
*/
|
|
94
|
+
export declare function claimProvisionRequest(grantId: string, now?: number): Promise<{
|
|
95
|
+
status: "ready";
|
|
96
|
+
code: string;
|
|
97
|
+
state: string;
|
|
98
|
+
} | {
|
|
99
|
+
status: "pending";
|
|
100
|
+
} | {
|
|
101
|
+
status: "none";
|
|
102
|
+
}>;
|
|
103
|
+
/** Drop a grant's request — used when the grant itself goes away. */
|
|
104
|
+
export declare function clearProvisionRequest(grantId: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* A state value for a borrower to send alongside its challenge.
|
|
107
|
+
*
|
|
108
|
+
* Kept here so both ends agree on the shape `STATE_PATTERN` will accept.
|
|
109
|
+
*/
|
|
110
|
+
export declare function generateProvisionState(): string;
|