@xenosystem/agent-cli 0.5.106 → 0.5.108
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/dist/cli.js +1 -1
- package/dist/index.js +1916 -1903
- package/dist/metafile-esm.json +1 -1
- package/dist/sdk-build-identity.json +8 -8
- package/native/xeno-pty/prebuilds/win32-x64/manifest.json +18 -18
- package/node_modules/@xeno-corporation/account/LICENSE +39 -0
- package/node_modules/@xeno-corporation/account/README.md +211 -0
- package/node_modules/@xeno-corporation/account/dist/chunk-FELZILY4.mjs +60 -0
- package/node_modules/@xeno-corporation/account/dist/chunk-FELZILY4.mjs.map +1 -0
- package/node_modules/@xeno-corporation/account/dist/index.d.mts +883 -0
- package/node_modules/@xeno-corporation/account/dist/index.d.ts +883 -0
- package/node_modules/@xeno-corporation/account/dist/index.js +2301 -0
- package/node_modules/@xeno-corporation/account/dist/index.js.map +1 -0
- package/node_modules/@xeno-corporation/account/dist/index.mjs +2191 -0
- package/node_modules/@xeno-corporation/account/dist/index.mjs.map +1 -0
- package/node_modules/@xeno-corporation/account/dist/node.d.mts +35 -0
- package/node_modules/@xeno-corporation/account/dist/node.d.ts +35 -0
- package/node_modules/@xeno-corporation/account/dist/node.js +715 -0
- package/node_modules/@xeno-corporation/account/dist/node.js.map +1 -0
- package/node_modules/@xeno-corporation/account/dist/node.mjs +645 -0
- package/node_modules/@xeno-corporation/account/dist/node.mjs.map +1 -0
- package/node_modules/@xeno-corporation/account/dist/tokenStore-CnIo2ILq.d.mts +47 -0
- package/node_modules/@xeno-corporation/account/dist/tokenStore-CnIo2ILq.d.ts +47 -0
- package/node_modules/@xeno-corporation/account/package.json +60 -0
- package/node_modules/zod/LICENSE +21 -0
- package/node_modules/zod/README.md +3028 -0
- package/node_modules/zod/index.d.ts +2 -0
- package/node_modules/zod/lib/ZodError.d.ts +164 -0
- package/node_modules/zod/lib/ZodError.js +137 -0
- package/node_modules/zod/lib/__tests__/Mocker.d.ts +17 -0
- package/node_modules/zod/lib/__tests__/Mocker.js +57 -0
- package/node_modules/zod/lib/benchmarks/datetime.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/datetime.js +54 -0
- package/node_modules/zod/lib/benchmarks/discriminatedUnion.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/discriminatedUnion.js +79 -0
- package/node_modules/zod/lib/benchmarks/index.d.ts +1 -0
- package/node_modules/zod/lib/benchmarks/index.js +59 -0
- package/node_modules/zod/lib/benchmarks/ipv4.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/ipv4.js +54 -0
- package/node_modules/zod/lib/benchmarks/object.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/object.js +70 -0
- package/node_modules/zod/lib/benchmarks/primitives.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/primitives.js +170 -0
- package/node_modules/zod/lib/benchmarks/realworld.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/realworld.js +56 -0
- package/node_modules/zod/lib/benchmarks/string.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/string.js +55 -0
- package/node_modules/zod/lib/benchmarks/union.d.ts +5 -0
- package/node_modules/zod/lib/benchmarks/union.js +79 -0
- package/node_modules/zod/lib/errors.d.ts +5 -0
- package/node_modules/zod/lib/errors.js +17 -0
- package/node_modules/zod/lib/external.d.ts +6 -0
- package/node_modules/zod/lib/external.js +22 -0
- package/node_modules/zod/lib/helpers/enumUtil.d.ts +8 -0
- package/node_modules/zod/lib/helpers/enumUtil.js +2 -0
- package/node_modules/zod/lib/helpers/errorUtil.d.ts +9 -0
- package/node_modules/zod/lib/helpers/errorUtil.js +8 -0
- package/node_modules/zod/lib/helpers/parseUtil.d.ts +78 -0
- package/node_modules/zod/lib/helpers/parseUtil.js +125 -0
- package/node_modules/zod/lib/helpers/partialUtil.d.ts +8 -0
- package/node_modules/zod/lib/helpers/partialUtil.js +2 -0
- package/node_modules/zod/lib/helpers/typeAliases.d.ts +2 -0
- package/node_modules/zod/lib/helpers/typeAliases.js +2 -0
- package/node_modules/zod/lib/helpers/util.d.ts +82 -0
- package/node_modules/zod/lib/helpers/util.js +142 -0
- package/node_modules/zod/lib/index.d.ts +4 -0
- package/node_modules/zod/lib/index.js +33 -0
- package/node_modules/zod/lib/index.mjs +4382 -0
- package/node_modules/zod/lib/index.umd.js +4497 -0
- package/node_modules/zod/lib/locales/en.d.ts +3 -0
- package/node_modules/zod/lib/locales/en.js +129 -0
- package/node_modules/zod/lib/standard-schema.d.ts +102 -0
- package/node_modules/zod/lib/standard-schema.js +2 -0
- package/node_modules/zod/lib/types.d.ts +1062 -0
- package/node_modules/zod/lib/types.js +3823 -0
- package/node_modules/zod/package.json +122 -0
- package/package.json +2 -2
|
@@ -0,0 +1,2301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
AccountClient: () => AccountClient,
|
|
25
|
+
AccountError: () => AccountError,
|
|
26
|
+
AuthzClient: () => AuthzClient,
|
|
27
|
+
BffSessionClient: () => BffSessionClient,
|
|
28
|
+
LedgerClient: () => LedgerClient,
|
|
29
|
+
MICRO_PER_CREDIT: () => MICRO_PER_CREDIT,
|
|
30
|
+
Transport: () => Transport,
|
|
31
|
+
XenoAccountClient: () => XenoAccountClient,
|
|
32
|
+
accountSchema: () => accountSchema,
|
|
33
|
+
authzCheckSchema: () => authzCheckSchema,
|
|
34
|
+
authzResultSchema: () => authzResultSchema,
|
|
35
|
+
balanceSchema: () => balanceSchema,
|
|
36
|
+
buildAuthorizeUrl: () => buildAuthorizeUrl,
|
|
37
|
+
codeForStatus: () => codeForStatus,
|
|
38
|
+
createOidcIntrospectionSessionAuthority: () => createOidcIntrospectionSessionAuthority,
|
|
39
|
+
createResourceRequestAuthorizer: () => createResourceRequestAuthorizer,
|
|
40
|
+
exchangeCode: () => exchangeCode,
|
|
41
|
+
fromMicro: () => fromMicro,
|
|
42
|
+
generatePkce: () => generatePkce,
|
|
43
|
+
generateState: () => generateState,
|
|
44
|
+
holdRequestSchema: () => holdRequestSchema,
|
|
45
|
+
holdSchema: () => holdSchema,
|
|
46
|
+
keychainStore: () => keychainStore,
|
|
47
|
+
memoryStore: () => memoryStore,
|
|
48
|
+
meterAggregationSchema: () => meterAggregationSchema,
|
|
49
|
+
normalize: () => normalize,
|
|
50
|
+
normalizeDpopHtu: () => normalizeDpopHtu,
|
|
51
|
+
oauthTokenResponseSchema: () => oauthTokenResponseSchema,
|
|
52
|
+
pollDeviceToken: () => pollDeviceToken,
|
|
53
|
+
redact: () => redact,
|
|
54
|
+
refreshTokens: () => refreshTokens,
|
|
55
|
+
startDeviceAuthorization: () => startDeviceAuthorization,
|
|
56
|
+
surfaceSchema: () => surfaceSchema,
|
|
57
|
+
toMicro: () => toMicro,
|
|
58
|
+
tokenSetSchema: () => tokenSetSchema,
|
|
59
|
+
usageEventSchema: () => usageEventSchema,
|
|
60
|
+
usageResultSchema: () => usageResultSchema,
|
|
61
|
+
verifyAccessToken: () => verifyAccessToken,
|
|
62
|
+
verifyIdToken: () => verifyIdToken,
|
|
63
|
+
verifyLogoutToken: () => verifyLogoutToken
|
|
64
|
+
});
|
|
65
|
+
module.exports = __toCommonJS(src_exports);
|
|
66
|
+
|
|
67
|
+
// src/types.ts
|
|
68
|
+
var import_zod = require("zod");
|
|
69
|
+
var MICRO_PER_CREDIT = 1e6;
|
|
70
|
+
var surfaceSchema = import_zod.z.string().min(2).max(64).regex(/^xeno[_a-z0-9]+$/, "surface must look like xeno_post");
|
|
71
|
+
var accountSchema = import_zod.z.object({
|
|
72
|
+
id: import_zod.z.string().min(1),
|
|
73
|
+
email: import_zod.z.string().email(),
|
|
74
|
+
username: import_zod.z.string().nullable().default(null),
|
|
75
|
+
displayName: import_zod.z.string().nullable().default(null),
|
|
76
|
+
avatarUrl: import_zod.z.string().url().nullable().default(null),
|
|
77
|
+
emailVerified: import_zod.z.boolean().default(false),
|
|
78
|
+
/** Linked app surfaces for this account (unified-account view). */
|
|
79
|
+
linkedSurfaces: import_zod.z.array(surfaceSchema).default([])
|
|
80
|
+
});
|
|
81
|
+
var tokenSetSchema = import_zod.z.object({
|
|
82
|
+
accessToken: import_zod.z.string().min(1),
|
|
83
|
+
/** Opaque or JWT; the SDK treats it as opaque. */
|
|
84
|
+
tokenType: import_zod.z.string().default("Bearer"),
|
|
85
|
+
expiresAt: import_zod.z.coerce.date().nullable().default(null),
|
|
86
|
+
refreshToken: import_zod.z.string().nullable().default(null),
|
|
87
|
+
/** OIDC ID token (authentication only — never sent to resource APIs). */
|
|
88
|
+
idToken: import_zod.z.string().nullable().default(null),
|
|
89
|
+
scope: import_zod.z.array(import_zod.z.string()).default([]),
|
|
90
|
+
/** Session id claim, the join key for back-channel logout. */
|
|
91
|
+
sid: import_zod.z.string().nullable().default(null),
|
|
92
|
+
/** Signature-verified claims; never populated by unverified decoding. */
|
|
93
|
+
accessClaims: import_zod.z.record(import_zod.z.unknown()).nullable().default(null),
|
|
94
|
+
idClaims: import_zod.z.record(import_zod.z.unknown()).nullable().default(null)
|
|
95
|
+
});
|
|
96
|
+
var oauthTokenResponseSchema = import_zod.z.object({
|
|
97
|
+
access_token: import_zod.z.string(),
|
|
98
|
+
token_type: import_zod.z.string().default("Bearer"),
|
|
99
|
+
expires_in: import_zod.z.number().optional(),
|
|
100
|
+
refresh_token: import_zod.z.string().optional(),
|
|
101
|
+
id_token: import_zod.z.string().optional(),
|
|
102
|
+
scope: import_zod.z.string().optional()
|
|
103
|
+
});
|
|
104
|
+
var balanceSchema = import_zod.z.object({
|
|
105
|
+
/** Settled balance. */
|
|
106
|
+
postedMicro: import_zod.z.number().int(),
|
|
107
|
+
/** Settled minus active holds. */
|
|
108
|
+
pendingMicro: import_zod.z.number().int(),
|
|
109
|
+
/** Most conservative spendable amount — gate approvals on this. */
|
|
110
|
+
availableMicro: import_zod.z.number().int(),
|
|
111
|
+
currency: import_zod.z.literal("credits").default("credits"),
|
|
112
|
+
asOf: import_zod.z.coerce.date().default(() => /* @__PURE__ */ new Date(0))
|
|
113
|
+
});
|
|
114
|
+
var meterAggregationSchema = import_zod.z.enum([
|
|
115
|
+
"sum",
|
|
116
|
+
"count",
|
|
117
|
+
"max",
|
|
118
|
+
"min",
|
|
119
|
+
"unique_count",
|
|
120
|
+
"latest"
|
|
121
|
+
]);
|
|
122
|
+
var usageEventSchema = import_zod.z.object({
|
|
123
|
+
surface: surfaceSchema,
|
|
124
|
+
/** Stable, producer-supplied dedup id. */
|
|
125
|
+
transactionId: import_zod.z.string().min(8).max(255),
|
|
126
|
+
operation: import_zod.z.string().min(1),
|
|
127
|
+
model: import_zod.z.string().nullable().default(null),
|
|
128
|
+
provider: import_zod.z.string().nullable().default(null),
|
|
129
|
+
/** Pre-computed cost in micro-credits, if the caller priced it. */
|
|
130
|
+
costMicro: import_zod.z.number().int().nonnegative().nullable().default(null),
|
|
131
|
+
inputTokens: import_zod.z.number().int().nonnegative().nullable().default(null),
|
|
132
|
+
outputTokens: import_zod.z.number().int().nonnegative().nullable().default(null),
|
|
133
|
+
/** Arbitrary attribution dimensions (project, workspace, …). */
|
|
134
|
+
dimensions: import_zod.z.record(import_zod.z.string()).default({}),
|
|
135
|
+
occurredAt: import_zod.z.coerce.date().optional()
|
|
136
|
+
});
|
|
137
|
+
var usageResultSchema = import_zod.z.object({
|
|
138
|
+
accepted: import_zod.z.boolean(),
|
|
139
|
+
/** True when the event was a duplicate (idempotent no-op). */
|
|
140
|
+
duplicate: import_zod.z.boolean().default(false),
|
|
141
|
+
costMicro: import_zod.z.number().int().nonnegative(),
|
|
142
|
+
transactionId: import_zod.z.string(),
|
|
143
|
+
balance: balanceSchema.nullable().default(null)
|
|
144
|
+
});
|
|
145
|
+
var holdRequestSchema = import_zod.z.object({
|
|
146
|
+
surface: surfaceSchema,
|
|
147
|
+
/** Idempotency key for the hold itself. */
|
|
148
|
+
holdId: import_zod.z.string().min(8).max(255),
|
|
149
|
+
amountMicro: import_zod.z.number().int().positive(),
|
|
150
|
+
operation: import_zod.z.string().min(1),
|
|
151
|
+
expiresInSeconds: import_zod.z.number().int().positive().max(86400).default(900),
|
|
152
|
+
dimensions: import_zod.z.record(import_zod.z.string()).default({})
|
|
153
|
+
});
|
|
154
|
+
var holdSchema = import_zod.z.object({
|
|
155
|
+
holdId: import_zod.z.string(),
|
|
156
|
+
state: import_zod.z.enum([
|
|
157
|
+
"held",
|
|
158
|
+
"settled",
|
|
159
|
+
"voided",
|
|
160
|
+
"expired"
|
|
161
|
+
]),
|
|
162
|
+
amountMicro: import_zod.z.number().int().nonnegative(),
|
|
163
|
+
settledMicro: import_zod.z.number().int().nonnegative().default(0),
|
|
164
|
+
balance: balanceSchema.nullable().default(null)
|
|
165
|
+
});
|
|
166
|
+
var authzCheckSchema = import_zod.z.object({
|
|
167
|
+
/** e.g. `user:abc` or `agent:scout`. */
|
|
168
|
+
subject: import_zod.z.string().min(1),
|
|
169
|
+
relation: import_zod.z.string().min(1),
|
|
170
|
+
/** e.g. `workspace:acme` or `post:123`. */
|
|
171
|
+
object: import_zod.z.string().min(1),
|
|
172
|
+
context: import_zod.z.record(import_zod.z.unknown()).default({})
|
|
173
|
+
});
|
|
174
|
+
var authzResultSchema = import_zod.z.object({
|
|
175
|
+
allowed: import_zod.z.boolean(),
|
|
176
|
+
/** Optional reason/trace for debugging (never authoritative). */
|
|
177
|
+
reason: import_zod.z.string().nullable().default(null)
|
|
178
|
+
});
|
|
179
|
+
function toMicro(credits) {
|
|
180
|
+
return Math.round(credits * MICRO_PER_CREDIT);
|
|
181
|
+
}
|
|
182
|
+
__name(toMicro, "toMicro");
|
|
183
|
+
function fromMicro(micro) {
|
|
184
|
+
return micro / MICRO_PER_CREDIT;
|
|
185
|
+
}
|
|
186
|
+
__name(fromMicro, "fromMicro");
|
|
187
|
+
|
|
188
|
+
// src/errors.ts
|
|
189
|
+
var AccountError = class extends Error {
|
|
190
|
+
static {
|
|
191
|
+
__name(this, "AccountError");
|
|
192
|
+
}
|
|
193
|
+
code;
|
|
194
|
+
httpStatus;
|
|
195
|
+
retryable;
|
|
196
|
+
retryAfterMs;
|
|
197
|
+
oauthError;
|
|
198
|
+
constructor(args) {
|
|
199
|
+
super(args.message);
|
|
200
|
+
this.name = "AccountError";
|
|
201
|
+
this.code = args.code;
|
|
202
|
+
this.httpStatus = args.httpStatus ?? null;
|
|
203
|
+
this.retryable = args.retryable ?? defaultRetryable(args.code);
|
|
204
|
+
this.retryAfterMs = args.retryAfterMs ?? null;
|
|
205
|
+
this.oauthError = args.oauthError ?? null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function defaultRetryable(code) {
|
|
209
|
+
return code === "UNREACHABLE" || code === "RATE_LIMITED" || code === "PLATFORM_ERROR";
|
|
210
|
+
}
|
|
211
|
+
__name(defaultRetryable, "defaultRetryable");
|
|
212
|
+
function codeForStatus(status) {
|
|
213
|
+
if (status === 401 || status === 403) return "INVALID_TOKEN";
|
|
214
|
+
if (status === 402) return "INSUFFICIENT_CREDITS";
|
|
215
|
+
if (status === 409) return "CONFLICT";
|
|
216
|
+
if (status === 400 || status === 422) return "BAD_REQUEST";
|
|
217
|
+
if (status === 404) return "NOT_FOUND";
|
|
218
|
+
if (status === 429) return "RATE_LIMITED";
|
|
219
|
+
if (status >= 500) return "UNREACHABLE";
|
|
220
|
+
return "PLATFORM_ERROR";
|
|
221
|
+
}
|
|
222
|
+
__name(codeForStatus, "codeForStatus");
|
|
223
|
+
|
|
224
|
+
// src/jwt-json.ts
|
|
225
|
+
function parseJwtObject(source) {
|
|
226
|
+
if (source.length > 16384) throw new SyntaxError("JWT JSON bound");
|
|
227
|
+
let offset = 0;
|
|
228
|
+
const whitespace = /* @__PURE__ */ __name(() => {
|
|
229
|
+
while (offset < source.length && /[ \t\r\n]/.test(source[offset] ?? "")) offset++;
|
|
230
|
+
}, "whitespace");
|
|
231
|
+
const invalid2 = /* @__PURE__ */ __name(() => {
|
|
232
|
+
throw new SyntaxError("Ambiguous JWT JSON");
|
|
233
|
+
}, "invalid");
|
|
234
|
+
const string = /* @__PURE__ */ __name(() => {
|
|
235
|
+
const start = offset++;
|
|
236
|
+
while (offset < source.length) {
|
|
237
|
+
const current = source[offset++];
|
|
238
|
+
if (current === '"') return JSON.parse(source.slice(start, offset));
|
|
239
|
+
if (current === "\\") offset++;
|
|
240
|
+
}
|
|
241
|
+
return invalid2();
|
|
242
|
+
}, "string");
|
|
243
|
+
const value = /* @__PURE__ */ __name((depth) => {
|
|
244
|
+
if (depth > 8) invalid2();
|
|
245
|
+
whitespace();
|
|
246
|
+
const opening = source[offset];
|
|
247
|
+
if (opening === '"') {
|
|
248
|
+
string();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (opening === "{" || opening === "[") {
|
|
252
|
+
const close = opening === "{" ? "}" : "]";
|
|
253
|
+
offset++;
|
|
254
|
+
whitespace();
|
|
255
|
+
if (source[offset] === close) {
|
|
256
|
+
offset++;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const names = /* @__PURE__ */ new Set();
|
|
260
|
+
let count = 0;
|
|
261
|
+
while (offset < source.length) {
|
|
262
|
+
if (++count > 256) invalid2();
|
|
263
|
+
if (opening === "{") {
|
|
264
|
+
whitespace();
|
|
265
|
+
if (source[offset] !== '"') invalid2();
|
|
266
|
+
const key = string();
|
|
267
|
+
if (names.has(key) || key === "__proto__" || key === "constructor" || key === "prototype") invalid2();
|
|
268
|
+
names.add(key);
|
|
269
|
+
whitespace();
|
|
270
|
+
if (source[offset++] !== ":") invalid2();
|
|
271
|
+
}
|
|
272
|
+
value(depth + 1);
|
|
273
|
+
whitespace();
|
|
274
|
+
if (source[offset] === close) {
|
|
275
|
+
offset++;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (source[offset++] !== ",") invalid2();
|
|
279
|
+
}
|
|
280
|
+
return invalid2();
|
|
281
|
+
}
|
|
282
|
+
const literal = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(source.slice(offset));
|
|
283
|
+
if (!literal) return invalid2();
|
|
284
|
+
offset += literal[0].length;
|
|
285
|
+
}, "value");
|
|
286
|
+
value(0);
|
|
287
|
+
whitespace();
|
|
288
|
+
if (offset !== source.length) invalid2();
|
|
289
|
+
const parsed = JSON.parse(source, (_key, item) => {
|
|
290
|
+
if (typeof item === "number" && !Number.isFinite(item)) invalid2();
|
|
291
|
+
return item;
|
|
292
|
+
});
|
|
293
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) invalid2();
|
|
294
|
+
return parsed;
|
|
295
|
+
}
|
|
296
|
+
__name(parseJwtObject, "parseJwtObject");
|
|
297
|
+
|
|
298
|
+
// src/http.ts
|
|
299
|
+
function authorizationArgs(auth) {
|
|
300
|
+
return typeof auth === "function" ? {
|
|
301
|
+
authorization: auth
|
|
302
|
+
} : typeof auth === "string" ? {
|
|
303
|
+
token: auth,
|
|
304
|
+
tokenType: "Bearer"
|
|
305
|
+
} : {
|
|
306
|
+
token: auth.token,
|
|
307
|
+
tokenType: auth.tokenType,
|
|
308
|
+
headers: auth.headers
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
__name(authorizationArgs, "authorizationArgs");
|
|
312
|
+
var Transport = class {
|
|
313
|
+
static {
|
|
314
|
+
__name(this, "Transport");
|
|
315
|
+
}
|
|
316
|
+
fetchImpl;
|
|
317
|
+
timeoutMs;
|
|
318
|
+
maxRetries;
|
|
319
|
+
userAgent;
|
|
320
|
+
maxResponseBytes;
|
|
321
|
+
constructor(opts = {}) {
|
|
322
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
323
|
+
this.timeoutMs = opts.timeoutMs ?? 8e3;
|
|
324
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
325
|
+
this.userAgent = opts.userAgent ?? "xeno-account-client/0.1";
|
|
326
|
+
this.maxResponseBytes = opts.maxResponseBytes ?? 1024 * 1024;
|
|
327
|
+
if (!Number.isSafeInteger(this.timeoutMs) || this.timeoutMs < 1 || this.timeoutMs > 12e4 || !Number.isSafeInteger(this.maxResponseBytes) || this.maxResponseBytes < 1 || this.maxResponseBytes > 8 * 1024 * 1024 || !Number.isSafeInteger(this.maxRetries) || this.maxRetries < 0 || this.maxRetries > 10) throw new TypeError("Account transport deadline and response bound are invalid");
|
|
328
|
+
}
|
|
329
|
+
/** Perform a request and return parsed JSON (or null for empty bodies). */
|
|
330
|
+
async request(args) {
|
|
331
|
+
let body = args.body;
|
|
332
|
+
const headers = {
|
|
333
|
+
...args.headers
|
|
334
|
+
};
|
|
335
|
+
if (body !== void 0 && typeof body !== "string") {
|
|
336
|
+
try {
|
|
337
|
+
body = JSON.stringify(body);
|
|
338
|
+
} catch {
|
|
339
|
+
throw new AccountError({
|
|
340
|
+
code: "BAD_REQUEST",
|
|
341
|
+
message: "Account request body cannot be serialized"
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
headers["content-type"] = "application/json";
|
|
345
|
+
}
|
|
346
|
+
args = Object.freeze({
|
|
347
|
+
...args,
|
|
348
|
+
body,
|
|
349
|
+
headers: Object.freeze(headers)
|
|
350
|
+
});
|
|
351
|
+
const isMutation = args.method !== "GET";
|
|
352
|
+
const staticProof = args.tokenType === "DPoP" || Object.keys(args.headers ?? {}).some((name) => name.toLowerCase() === "dpop");
|
|
353
|
+
const retrySafe = (!isMutation || Boolean(args.idempotencyKey)) && (!staticProof || Boolean(args.authorization));
|
|
354
|
+
const attempts = retrySafe ? this.maxRetries + 1 : 1;
|
|
355
|
+
let lastErr = null;
|
|
356
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
357
|
+
try {
|
|
358
|
+
return await this.once(args);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
const ae = err instanceof AccountError ? err : new AccountError({
|
|
361
|
+
code: "UNREACHABLE",
|
|
362
|
+
message: "Account request failed before a verified response was received"
|
|
363
|
+
});
|
|
364
|
+
lastErr = ae;
|
|
365
|
+
if (!ae.retryable || attempt === attempts - 1) throw ae;
|
|
366
|
+
await sleep(backoffMs(attempt, ae.retryAfterMs), args.signal);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
throw lastErr ?? new AccountError({
|
|
370
|
+
code: "PLATFORM_ERROR",
|
|
371
|
+
message: "request failed"
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async once(args) {
|
|
375
|
+
const controller = new AbortController();
|
|
376
|
+
const callerAborted = /* @__PURE__ */ __name(() => controller.abort(), "callerAborted");
|
|
377
|
+
args.signal?.addEventListener("abort", callerAborted, {
|
|
378
|
+
once: true
|
|
379
|
+
});
|
|
380
|
+
if (args.signal?.aborted) callerAborted();
|
|
381
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
382
|
+
let res;
|
|
383
|
+
try {
|
|
384
|
+
if (args.signal?.aborted) throw cancellationError();
|
|
385
|
+
let auth;
|
|
386
|
+
if (args.authorization) {
|
|
387
|
+
try {
|
|
388
|
+
auth = await beforeAbort(args.authorization(args.method, args.url), controller.signal);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw new AccountError({
|
|
391
|
+
code: error instanceof AccountError ? error.code : "UNREACHABLE",
|
|
392
|
+
message: "Account request authorization unavailable",
|
|
393
|
+
...error instanceof AccountError ? {
|
|
394
|
+
httpStatus: error.httpStatus,
|
|
395
|
+
retryable: error.retryable
|
|
396
|
+
} : {}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (!auth || typeof auth.token !== "string" || !auth.token || auth.token.length > 16384 || /[\r\n]/.test(auth.token) || ![
|
|
400
|
+
"Bearer",
|
|
401
|
+
"DPoP"
|
|
402
|
+
].includes(auth.tokenType)) throw new AccountError({
|
|
403
|
+
code: "INVALID_TOKEN",
|
|
404
|
+
message: "Account request authorization invalid"
|
|
405
|
+
});
|
|
406
|
+
const proofHeaders = Object.entries(auth.headers ?? {}).filter(([name]) => name.toLowerCase() === "dpop");
|
|
407
|
+
if (auth.tokenType === "DPoP" && (proofHeaders.length !== 1 || typeof proofHeaders[0]?.[1] !== "string" || !proofHeaders[0][1] || proofHeaders[0][1].length > 16384 || /[\r\n]/.test(proofHeaders[0][1]))) throw new AccountError({
|
|
408
|
+
code: "INVALID_TOKEN",
|
|
409
|
+
message: "Account request proof invalid"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const supplied = Object.fromEntries(Object.entries(args.headers ?? {}).map(([key, value]) => [
|
|
413
|
+
key.toLowerCase(),
|
|
414
|
+
value
|
|
415
|
+
]));
|
|
416
|
+
if (auth) {
|
|
417
|
+
delete supplied.authorization;
|
|
418
|
+
delete supplied.dpop;
|
|
419
|
+
}
|
|
420
|
+
const headers = {
|
|
421
|
+
accept: "application/json",
|
|
422
|
+
"user-agent": this.userAgent,
|
|
423
|
+
...supplied,
|
|
424
|
+
...Object.fromEntries(Object.entries(auth?.headers ?? {}).map(([key, value]) => [
|
|
425
|
+
key.toLowerCase(),
|
|
426
|
+
value
|
|
427
|
+
]))
|
|
428
|
+
};
|
|
429
|
+
const token = auth?.token ?? args.token;
|
|
430
|
+
if (token) headers["authorization"] = `${auth?.tokenType ?? args.tokenType ?? "Bearer"} ${token}`;
|
|
431
|
+
if (args.idempotencyKey) headers["idempotency-key"] = args.idempotencyKey;
|
|
432
|
+
let payload;
|
|
433
|
+
if (typeof args.body === "string") {
|
|
434
|
+
payload = args.body;
|
|
435
|
+
} else if (args.body !== void 0) {
|
|
436
|
+
headers["content-type"] = "application/json";
|
|
437
|
+
try {
|
|
438
|
+
payload = JSON.stringify(args.body);
|
|
439
|
+
} catch {
|
|
440
|
+
throw new AccountError({
|
|
441
|
+
code: "BAD_REQUEST",
|
|
442
|
+
message: "Account request body cannot be serialized"
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (controller.signal.aborted) throw new Error("Account response deadline");
|
|
447
|
+
res = await fetchBeforeAbort(this.fetchImpl, args.url, {
|
|
448
|
+
method: args.method,
|
|
449
|
+
headers,
|
|
450
|
+
body: payload,
|
|
451
|
+
signal: controller.signal,
|
|
452
|
+
redirect: "error",
|
|
453
|
+
cache: "no-store",
|
|
454
|
+
credentials: "omit"
|
|
455
|
+
}, controller.signal);
|
|
456
|
+
if (!res.ok) {
|
|
457
|
+
const code = codeForStatus(res.status);
|
|
458
|
+
const oauthError = args.oauthErrorResponse ? await readOAuthError(res, controller.signal) : null;
|
|
459
|
+
throw new AccountError({
|
|
460
|
+
code,
|
|
461
|
+
message: `${args.method} ${safeUrl(args.url)} -> ${res.status}`,
|
|
462
|
+
httpStatus: res.status,
|
|
463
|
+
retryAfterMs: parseRetryAfter(res.headers.get("retry-after")),
|
|
464
|
+
oauthError
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (res.status === 204 || !res.body) return null;
|
|
468
|
+
const text = await readBoundedText(res, controller.signal, this.maxResponseBytes);
|
|
469
|
+
if (!text) return null;
|
|
470
|
+
try {
|
|
471
|
+
return JSON.parse(text);
|
|
472
|
+
} catch {
|
|
473
|
+
throw new AccountError({
|
|
474
|
+
code: "INVALID_RESPONSE",
|
|
475
|
+
message: `non-JSON response from ${safeUrl(args.url)}`
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (args.signal?.aborted) throw cancellationError();
|
|
480
|
+
if (err instanceof AccountError) throw err;
|
|
481
|
+
throw new AccountError({
|
|
482
|
+
code: "UNREACHABLE",
|
|
483
|
+
message: `request to ${safeUrl(args.url)} failed`,
|
|
484
|
+
retryable: true
|
|
485
|
+
});
|
|
486
|
+
} finally {
|
|
487
|
+
controller.abort();
|
|
488
|
+
args.signal?.removeEventListener("abort", callerAborted);
|
|
489
|
+
clearTimeout(timer);
|
|
490
|
+
if (res) cancelBody(res);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
function cancelBody(response) {
|
|
495
|
+
void response.body?.cancel().catch(() => {
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
__name(cancelBody, "cancelBody");
|
|
499
|
+
function beforeAbort(pending, signal) {
|
|
500
|
+
return new Promise((resolve, reject) => {
|
|
501
|
+
const aborted = /* @__PURE__ */ __name(() => {
|
|
502
|
+
signal.removeEventListener("abort", aborted);
|
|
503
|
+
reject(new Error("Account response deadline"));
|
|
504
|
+
}, "aborted");
|
|
505
|
+
if (signal.aborted) {
|
|
506
|
+
void pending.catch(() => {
|
|
507
|
+
});
|
|
508
|
+
aborted();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
signal.addEventListener("abort", aborted, {
|
|
512
|
+
once: true
|
|
513
|
+
});
|
|
514
|
+
pending.then((value) => {
|
|
515
|
+
signal.removeEventListener("abort", aborted);
|
|
516
|
+
if (signal.aborted) aborted();
|
|
517
|
+
else resolve(value);
|
|
518
|
+
}, (error) => {
|
|
519
|
+
signal.removeEventListener("abort", aborted);
|
|
520
|
+
reject(error);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
__name(beforeAbort, "beforeAbort");
|
|
525
|
+
function fetchBeforeAbort(fetchImpl, url, init, signal) {
|
|
526
|
+
const pending = Promise.resolve(fetchImpl(url, init));
|
|
527
|
+
void pending.then((response) => {
|
|
528
|
+
if (signal.aborted) cancelBody(response);
|
|
529
|
+
}, () => {
|
|
530
|
+
});
|
|
531
|
+
return beforeAbort(pending, signal);
|
|
532
|
+
}
|
|
533
|
+
__name(fetchBeforeAbort, "fetchBeforeAbort");
|
|
534
|
+
async function readBoundedText(response, signal, maxBytes) {
|
|
535
|
+
const reader = response.body?.getReader();
|
|
536
|
+
if (!reader) return "";
|
|
537
|
+
let complete = false;
|
|
538
|
+
try {
|
|
539
|
+
const decoder = new TextDecoder("utf-8", {
|
|
540
|
+
fatal: true
|
|
541
|
+
});
|
|
542
|
+
let text = "", size = 0;
|
|
543
|
+
while (true) {
|
|
544
|
+
const chunk = await beforeAbort(reader.read(), signal);
|
|
545
|
+
if (chunk.done) {
|
|
546
|
+
complete = true;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
size += chunk.value.byteLength;
|
|
550
|
+
if (size > maxBytes) throw new AccountError({
|
|
551
|
+
code: "INVALID_RESPONSE",
|
|
552
|
+
message: "Account response exceeds configured bound"
|
|
553
|
+
});
|
|
554
|
+
try {
|
|
555
|
+
text += decoder.decode(chunk.value, {
|
|
556
|
+
stream: true
|
|
557
|
+
});
|
|
558
|
+
} catch {
|
|
559
|
+
throw new AccountError({
|
|
560
|
+
code: "INVALID_RESPONSE",
|
|
561
|
+
message: "Account response is not valid UTF-8"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
return text + decoder.decode();
|
|
567
|
+
} catch {
|
|
568
|
+
throw new AccountError({
|
|
569
|
+
code: "INVALID_RESPONSE",
|
|
570
|
+
message: "Account response is not valid UTF-8"
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
} finally {
|
|
574
|
+
if (!complete) void reader.cancel().catch(() => {
|
|
575
|
+
});
|
|
576
|
+
reader.releaseLock();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
__name(readBoundedText, "readBoundedText");
|
|
580
|
+
async function readOAuthError(response, signal) {
|
|
581
|
+
try {
|
|
582
|
+
const body = parseJwtObject(await readBoundedText(response, signal, 4096));
|
|
583
|
+
const code = body.error;
|
|
584
|
+
return typeof code === "string" && [
|
|
585
|
+
"invalid_grant",
|
|
586
|
+
"invalid_client",
|
|
587
|
+
"invalid_scope",
|
|
588
|
+
"invalid_request",
|
|
589
|
+
"unauthorized_client",
|
|
590
|
+
"unsupported_grant_type",
|
|
591
|
+
"authorization_pending",
|
|
592
|
+
"slow_down",
|
|
593
|
+
"access_denied",
|
|
594
|
+
"expired_token",
|
|
595
|
+
"use_dpop_nonce",
|
|
596
|
+
"invalid_dpop_proof"
|
|
597
|
+
].includes(code) ? code : null;
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
__name(readOAuthError, "readOAuthError");
|
|
603
|
+
function backoffMs(attempt, retryAfterMs) {
|
|
604
|
+
if (retryAfterMs && retryAfterMs > 0) return Math.min(retryAfterMs, 1e4);
|
|
605
|
+
return Math.min(200 * (2 ** attempt + (attempt === 0 ? 0 : 1)), 5e3);
|
|
606
|
+
}
|
|
607
|
+
__name(backoffMs, "backoffMs");
|
|
608
|
+
function parseRetryAfter(header) {
|
|
609
|
+
if (!header) return null;
|
|
610
|
+
const seconds = Number(header);
|
|
611
|
+
return Number.isFinite(seconds) ? seconds * 1e3 : null;
|
|
612
|
+
}
|
|
613
|
+
__name(parseRetryAfter, "parseRetryAfter");
|
|
614
|
+
function cancellationError() {
|
|
615
|
+
return new AccountError({
|
|
616
|
+
code: "BAD_REQUEST",
|
|
617
|
+
message: "Account request was cancelled; outcome may be unconfirmed",
|
|
618
|
+
retryable: false
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
__name(cancellationError, "cancellationError");
|
|
622
|
+
function sleep(ms, signal) {
|
|
623
|
+
return new Promise((resolve, reject) => {
|
|
624
|
+
const timer = setTimeout(done, ms);
|
|
625
|
+
function cleanup() {
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
signal?.removeEventListener("abort", aborted);
|
|
628
|
+
}
|
|
629
|
+
__name(cleanup, "cleanup");
|
|
630
|
+
function done() {
|
|
631
|
+
cleanup();
|
|
632
|
+
resolve();
|
|
633
|
+
}
|
|
634
|
+
__name(done, "done");
|
|
635
|
+
function aborted() {
|
|
636
|
+
cleanup();
|
|
637
|
+
reject(cancellationError());
|
|
638
|
+
}
|
|
639
|
+
__name(aborted, "aborted");
|
|
640
|
+
signal?.addEventListener("abort", aborted, {
|
|
641
|
+
once: true
|
|
642
|
+
});
|
|
643
|
+
if (signal?.aborted) aborted();
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
__name(sleep, "sleep");
|
|
647
|
+
function safeUrl(url) {
|
|
648
|
+
try {
|
|
649
|
+
const u = new URL(url);
|
|
650
|
+
return u.origin;
|
|
651
|
+
} catch {
|
|
652
|
+
return "[invalid account URL]";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
__name(safeUrl, "safeUrl");
|
|
656
|
+
|
|
657
|
+
// src/account.ts
|
|
658
|
+
var AccountClient = class {
|
|
659
|
+
static {
|
|
660
|
+
__name(this, "AccountClient");
|
|
661
|
+
}
|
|
662
|
+
base;
|
|
663
|
+
transport;
|
|
664
|
+
constructor(base, transport) {
|
|
665
|
+
this.base = base;
|
|
666
|
+
this.transport = transport;
|
|
667
|
+
}
|
|
668
|
+
/** Resolve a token to its account, or throw an AccountError. */
|
|
669
|
+
async me(auth) {
|
|
670
|
+
let raw;
|
|
671
|
+
try {
|
|
672
|
+
raw = await this.transport.request({
|
|
673
|
+
method: "GET",
|
|
674
|
+
url: `${this.base}/api/v2/me`,
|
|
675
|
+
...authorizationArgs(auth)
|
|
676
|
+
});
|
|
677
|
+
} catch (err) {
|
|
678
|
+
const isDpop = typeof auth === "object" && auth.tokenType === "DPoP";
|
|
679
|
+
if (err instanceof AccountError && err.code === "NOT_FOUND" && !isDpop) {
|
|
680
|
+
raw = await this.transport.request({
|
|
681
|
+
method: "GET",
|
|
682
|
+
url: `${this.base}/api/auth/me`,
|
|
683
|
+
...authorizationArgs(auth)
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const account = normalize(raw);
|
|
690
|
+
if (!account) {
|
|
691
|
+
throw new AccountError({
|
|
692
|
+
code: "INVALID_RESPONSE",
|
|
693
|
+
message: "/me returned an unrecognised shape"
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return account;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
function normalize(body) {
|
|
700
|
+
if (!body || typeof body !== "object") return null;
|
|
701
|
+
const root = body;
|
|
702
|
+
const user = root["user"] ?? root["data"] ?? root;
|
|
703
|
+
if (!user || typeof user !== "object") return null;
|
|
704
|
+
const id = str(user["id"]);
|
|
705
|
+
const email = str(user["email"]);
|
|
706
|
+
if (!id || !email) return null;
|
|
707
|
+
const linked = Array.isArray(user["linkedSurfaces"]) ? [
|
|
708
|
+
...new Set(user["linkedSurfaces"].map(normalizeSurface).filter(isSurface))
|
|
709
|
+
] : [];
|
|
710
|
+
const parsed = accountSchema.safeParse({
|
|
711
|
+
id,
|
|
712
|
+
email,
|
|
713
|
+
username: str(user["username"]) ?? null,
|
|
714
|
+
displayName: str(user["display_name"]) ?? str(user["displayName"]) ?? null,
|
|
715
|
+
avatarUrl: str(user["avatar_url"]) ?? str(user["avatarUrl"]) ?? null,
|
|
716
|
+
emailVerified: Boolean(user["email_verified"] ?? user["emailVerified"] ?? false),
|
|
717
|
+
linkedSurfaces: linked
|
|
718
|
+
});
|
|
719
|
+
return parsed.success ? parsed.data : null;
|
|
720
|
+
}
|
|
721
|
+
__name(normalize, "normalize");
|
|
722
|
+
function str(v) {
|
|
723
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
724
|
+
}
|
|
725
|
+
__name(str, "str");
|
|
726
|
+
function normalizeSurface(value) {
|
|
727
|
+
if (typeof value !== "string") return void 0;
|
|
728
|
+
const parsed = surfaceSchema.safeParse(value.trim().toLowerCase().replace(/-/g, "_"));
|
|
729
|
+
return parsed.success ? parsed.data : void 0;
|
|
730
|
+
}
|
|
731
|
+
__name(normalizeSurface, "normalizeSurface");
|
|
732
|
+
function isSurface(value) {
|
|
733
|
+
return value !== void 0;
|
|
734
|
+
}
|
|
735
|
+
__name(isSurface, "isSurface");
|
|
736
|
+
|
|
737
|
+
// src/ledger.ts
|
|
738
|
+
var LedgerClient = class {
|
|
739
|
+
static {
|
|
740
|
+
__name(this, "LedgerClient");
|
|
741
|
+
}
|
|
742
|
+
apiBase;
|
|
743
|
+
transport;
|
|
744
|
+
constructor(apiBase, transport) {
|
|
745
|
+
this.apiBase = apiBase;
|
|
746
|
+
this.transport = transport;
|
|
747
|
+
}
|
|
748
|
+
/** Posted / pending / available balance for the token's account. */
|
|
749
|
+
async getBalance(auth) {
|
|
750
|
+
const raw = await this.transport.request({
|
|
751
|
+
method: "GET",
|
|
752
|
+
url: `${this.apiBase}/api/v2/ledger/balance`,
|
|
753
|
+
...authorizationArgs(auth)
|
|
754
|
+
});
|
|
755
|
+
return parse(balanceSchema, raw);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Record a usage event (the meter). Idempotent on `transactionId`; a duplicate
|
|
759
|
+
* returns `{ duplicate: true }` and does not charge again.
|
|
760
|
+
*/
|
|
761
|
+
async recordUsage(auth, event) {
|
|
762
|
+
const e = usageEventSchema.parse(event);
|
|
763
|
+
const raw = await this.transport.request({
|
|
764
|
+
method: "POST",
|
|
765
|
+
url: `${this.apiBase}/api/v2/ledger/usage`,
|
|
766
|
+
...authorizationArgs(auth),
|
|
767
|
+
body: e,
|
|
768
|
+
idempotencyKey: e.transactionId
|
|
769
|
+
});
|
|
770
|
+
return parse(usageResultSchema, raw);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Reserve credits for a per-use action (phase 1 of two-phase spend). Rejected
|
|
774
|
+
* with `INSUFFICIENT_CREDITS` if `available < amount`. Idempotent on `holdId`.
|
|
775
|
+
*/
|
|
776
|
+
async hold(auth, req) {
|
|
777
|
+
const r = holdRequestSchema.parse(req);
|
|
778
|
+
const raw = await this.transport.request({
|
|
779
|
+
method: "POST",
|
|
780
|
+
url: `${this.apiBase}/api/v2/ledger/holds`,
|
|
781
|
+
...authorizationArgs(auth),
|
|
782
|
+
body: r,
|
|
783
|
+
idempotencyKey: r.holdId
|
|
784
|
+
});
|
|
785
|
+
return parse(holdSchema, raw);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Settle a hold for the actual cost (phase 2). Posting less than the held
|
|
789
|
+
* amount auto-restores the remainder. Idempotent on `holdId`.
|
|
790
|
+
*/
|
|
791
|
+
async settle(auth, holdId, actualCostMicro) {
|
|
792
|
+
const raw = await this.transport.request({
|
|
793
|
+
method: "POST",
|
|
794
|
+
url: `${this.apiBase}/api/v2/ledger/holds/${encodeURIComponent(holdId)}/settle`,
|
|
795
|
+
...authorizationArgs(auth),
|
|
796
|
+
body: {
|
|
797
|
+
actualCostMicro
|
|
798
|
+
},
|
|
799
|
+
idempotencyKey: `${holdId}:settle`
|
|
800
|
+
});
|
|
801
|
+
return parse(holdSchema, raw);
|
|
802
|
+
}
|
|
803
|
+
/** Release a hold without charging (failure/cancel). Idempotent on `holdId`. */
|
|
804
|
+
async void(auth, holdId) {
|
|
805
|
+
const raw = await this.transport.request({
|
|
806
|
+
method: "POST",
|
|
807
|
+
url: `${this.apiBase}/api/v2/ledger/holds/${encodeURIComponent(holdId)}/void`,
|
|
808
|
+
...authorizationArgs(auth),
|
|
809
|
+
body: {},
|
|
810
|
+
idempotencyKey: `${holdId}:void`
|
|
811
|
+
});
|
|
812
|
+
return parse(holdSchema, raw);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
function parse(schema, raw) {
|
|
816
|
+
const r = schema.safeParse(raw);
|
|
817
|
+
if (!r.success || r.data === void 0) {
|
|
818
|
+
throw new AccountError({
|
|
819
|
+
code: "INVALID_RESPONSE",
|
|
820
|
+
message: "ledger returned an unrecognised shape"
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
return r.data;
|
|
824
|
+
}
|
|
825
|
+
__name(parse, "parse");
|
|
826
|
+
|
|
827
|
+
// src/authz.ts
|
|
828
|
+
var AuthzClient = class {
|
|
829
|
+
static {
|
|
830
|
+
__name(this, "AuthzClient");
|
|
831
|
+
}
|
|
832
|
+
apiBase;
|
|
833
|
+
transport;
|
|
834
|
+
constructor(apiBase, transport) {
|
|
835
|
+
this.apiBase = apiBase;
|
|
836
|
+
this.transport = transport;
|
|
837
|
+
}
|
|
838
|
+
async check(auth, check) {
|
|
839
|
+
const raw = await this.transport.request({
|
|
840
|
+
method: "POST",
|
|
841
|
+
url: `${this.apiBase}/api/v2/authz/check`,
|
|
842
|
+
...authorizationArgs(auth),
|
|
843
|
+
body: check
|
|
844
|
+
});
|
|
845
|
+
const parsed = authzResultSchema.safeParse(raw);
|
|
846
|
+
if (!parsed.success) {
|
|
847
|
+
throw new AccountError({
|
|
848
|
+
code: "INVALID_RESPONSE",
|
|
849
|
+
message: "authz returned an unrecognised shape"
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return parsed.data;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/verification.ts
|
|
857
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
858
|
+
function bytes(value) {
|
|
859
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
860
|
+
const bin = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
|
|
861
|
+
return Uint8Array.from(bin, (char) => char.charCodeAt(0));
|
|
862
|
+
}
|
|
863
|
+
__name(bytes, "bytes");
|
|
864
|
+
function decode(token) {
|
|
865
|
+
const parts = token.split(".");
|
|
866
|
+
if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) throw invalid("malformed JWT");
|
|
867
|
+
try {
|
|
868
|
+
return {
|
|
869
|
+
header: JSON.parse(new TextDecoder().decode(bytes(parts[0]))),
|
|
870
|
+
payload: JSON.parse(new TextDecoder().decode(bytes(parts[1]))),
|
|
871
|
+
signingInput: new TextEncoder().encode(`${parts[0]}.${parts[1]}`),
|
|
872
|
+
signature: bytes(parts[2])
|
|
873
|
+
};
|
|
874
|
+
} catch {
|
|
875
|
+
throw invalid("malformed JWT");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
__name(decode, "decode");
|
|
879
|
+
function invalid(message) {
|
|
880
|
+
return new AccountError({
|
|
881
|
+
code: "INVALID_RESPONSE",
|
|
882
|
+
message: `OIDC verification failed: ${message}`
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
__name(invalid, "invalid");
|
|
886
|
+
function audience(value, expected) {
|
|
887
|
+
return value === expected || Array.isArray(value) && value.includes(expected);
|
|
888
|
+
}
|
|
889
|
+
__name(audience, "audience");
|
|
890
|
+
async function fetchJwks(transport, issuer, jwksUrl, force = false) {
|
|
891
|
+
const url = jwksUrl ?? `${issuer.replace(/\/+$/, "")}/api/oauth2/jwks`;
|
|
892
|
+
const cached = jwksCache.get(url);
|
|
893
|
+
if (!force && cached && cached.expiresAt > Date.now()) return cached.keys;
|
|
894
|
+
const response = await transport.request({
|
|
895
|
+
method: "GET",
|
|
896
|
+
url
|
|
897
|
+
});
|
|
898
|
+
if (!Array.isArray(response.keys) || response.keys.length === 0) throw invalid("empty JWKS");
|
|
899
|
+
jwksCache.set(url, {
|
|
900
|
+
keys: response.keys,
|
|
901
|
+
expiresAt: Date.now() + 5 * 6e4
|
|
902
|
+
});
|
|
903
|
+
return response.keys;
|
|
904
|
+
}
|
|
905
|
+
__name(fetchJwks, "fetchJwks");
|
|
906
|
+
async function verifySignature(decoded, keys) {
|
|
907
|
+
if (decoded.header.alg !== "ES256" || typeof decoded.header.kid !== "string") return "invalid";
|
|
908
|
+
const jwk = keys.find((candidate) => candidate.kid === decoded.header.kid && candidate.kty === "EC" && candidate.crv === "P-256");
|
|
909
|
+
if (!jwk) return "unknown-kid";
|
|
910
|
+
try {
|
|
911
|
+
const key = await globalThis.crypto.subtle.importKey("jwk", jwk, {
|
|
912
|
+
name: "ECDSA",
|
|
913
|
+
namedCurve: "P-256"
|
|
914
|
+
}, false, [
|
|
915
|
+
"verify"
|
|
916
|
+
]);
|
|
917
|
+
return await globalThis.crypto.subtle.verify({
|
|
918
|
+
name: "ECDSA",
|
|
919
|
+
hash: "SHA-256"
|
|
920
|
+
}, key, decoded.signature, decoded.signingInput) ? "valid" : "invalid";
|
|
921
|
+
} catch {
|
|
922
|
+
return "invalid";
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
__name(verifySignature, "verifySignature");
|
|
926
|
+
async function verifiedPayload(token, transport, issuer, jwksUrl) {
|
|
927
|
+
const decoded = decode(token);
|
|
928
|
+
let keys = await fetchJwks(transport, issuer, jwksUrl);
|
|
929
|
+
let result = await verifySignature(decoded, keys);
|
|
930
|
+
if (result === "unknown-kid") {
|
|
931
|
+
keys = await fetchJwks(transport, issuer, jwksUrl, true);
|
|
932
|
+
result = await verifySignature(decoded, keys);
|
|
933
|
+
}
|
|
934
|
+
if (result !== "valid") throw invalid("signature or algorithm");
|
|
935
|
+
return decoded;
|
|
936
|
+
}
|
|
937
|
+
__name(verifiedPayload, "verifiedPayload");
|
|
938
|
+
function temporal(payload, now) {
|
|
939
|
+
if (!Number.isInteger(payload.exp) || Number(payload.exp) <= now) throw invalid("token expired");
|
|
940
|
+
if (!Number.isInteger(payload.iat) || Number(payload.iat) > now + 60) throw invalid("invalid issued-at time");
|
|
941
|
+
if (payload.nbf !== void 0 && (!Number.isInteger(payload.nbf) || Number(payload.nbf) > now + 60)) throw invalid("token not yet valid");
|
|
942
|
+
}
|
|
943
|
+
__name(temporal, "temporal");
|
|
944
|
+
async function verifyAccessToken(args) {
|
|
945
|
+
const decoded = await verifiedPayload(args.token, args.transport, args.issuer, args.jwksUrl);
|
|
946
|
+
const p = decoded.payload;
|
|
947
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
948
|
+
temporal(p, now);
|
|
949
|
+
if (decoded.header.typ !== "at+jwt" || p.typ !== "at+jwt" || p.iss !== args.issuer || !audience(p.aud, "xeno-api")) throw invalid("access-token profile");
|
|
950
|
+
if (p.client_id !== args.clientId || p.azp !== args.clientId) throw invalid("client binding");
|
|
951
|
+
if (typeof p.sub !== "string" || typeof p.sid !== "string" || !Number.isInteger(p.auth_epoch)) throw invalid("session claims");
|
|
952
|
+
if (!p.cnf || typeof p.cnf !== "object" || p.cnf.jkt !== args.dpopJkt) throw invalid("DPoP binding");
|
|
953
|
+
if (p.act !== void 0) {
|
|
954
|
+
const act = p.act;
|
|
955
|
+
if (act.client_id !== "xeno-hub" || typeof act.installation_id_hash !== "string" || !act.installation_id_hash) throw invalid("broker actor");
|
|
956
|
+
}
|
|
957
|
+
if (args.requireBrokerActor && p.act === void 0) throw invalid("missing broker actor");
|
|
958
|
+
const held = new Set(String(p.scope ?? "").split(/\s+/).filter(Boolean));
|
|
959
|
+
for (const scope of args.expectedScopes ?? []) if (!held.has(scope)) throw invalid(`missing scope ${scope}`);
|
|
960
|
+
return p;
|
|
961
|
+
}
|
|
962
|
+
__name(verifyAccessToken, "verifyAccessToken");
|
|
963
|
+
async function verifyIdToken(args) {
|
|
964
|
+
const decoded = await verifiedPayload(args.token, args.transport, args.issuer, args.jwksUrl);
|
|
965
|
+
const p = decoded.payload;
|
|
966
|
+
temporal(p, Math.floor(Date.now() / 1e3));
|
|
967
|
+
if (p.iss !== args.issuer || !audience(p.aud, args.clientId) || typeof p.sub !== "string" || typeof p.sid !== "string" || !Number.isInteger(p.auth_time)) throw invalid("ID-token profile");
|
|
968
|
+
if (args.expectedNonce !== void 0 && p.nonce !== args.expectedNonce) throw invalid("nonce mismatch");
|
|
969
|
+
if (args.expectedSubject !== void 0 && p.sub !== args.expectedSubject) throw invalid("subject mismatch");
|
|
970
|
+
return p;
|
|
971
|
+
}
|
|
972
|
+
__name(verifyIdToken, "verifyIdToken");
|
|
973
|
+
async function verifyLogoutToken(args) {
|
|
974
|
+
const decoded = await verifiedPayload(args.token, args.transport, args.issuer, args.jwksUrl);
|
|
975
|
+
const p = decoded.payload;
|
|
976
|
+
temporal(p, Math.floor(Date.now() / 1e3));
|
|
977
|
+
const event = "http://schemas.openid.net/event/backchannel-logout";
|
|
978
|
+
if (decoded.header.typ !== "logout+jwt" || p.iss !== args.issuer || !audience(p.aud, args.clientId)) throw invalid("logout-token profile");
|
|
979
|
+
if (typeof p.jti !== "string" || !p.jti || p.nonce !== void 0) throw invalid("logout-token claims");
|
|
980
|
+
if (typeof p.sid !== "string" && typeof p.sub !== "string") throw invalid("logout subject");
|
|
981
|
+
if (!p.events || typeof p.events !== "object" || !(event in p.events)) throw invalid("logout event");
|
|
982
|
+
return p;
|
|
983
|
+
}
|
|
984
|
+
__name(verifyLogoutToken, "verifyLogoutToken");
|
|
985
|
+
|
|
986
|
+
// src/oidc.ts
|
|
987
|
+
async function generatePkce() {
|
|
988
|
+
const verifierBytes = new Uint8Array(32);
|
|
989
|
+
globalThis.crypto.getRandomValues(verifierBytes);
|
|
990
|
+
const verifier = base64url(verifierBytes);
|
|
991
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
992
|
+
return {
|
|
993
|
+
verifier,
|
|
994
|
+
challenge: base64url(new Uint8Array(digest))
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
__name(generatePkce, "generatePkce");
|
|
998
|
+
function generateState() {
|
|
999
|
+
const b = new Uint8Array(24);
|
|
1000
|
+
globalThis.crypto.getRandomValues(b);
|
|
1001
|
+
return base64url(b);
|
|
1002
|
+
}
|
|
1003
|
+
__name(generateState, "generateState");
|
|
1004
|
+
function base64url(bytes2) {
|
|
1005
|
+
let bin = "";
|
|
1006
|
+
for (let i = 0; i < bytes2.length; i += 1) bin += String.fromCharCode(bytes2[i]);
|
|
1007
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bytes2).toString("base64");
|
|
1008
|
+
return b64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1009
|
+
}
|
|
1010
|
+
__name(base64url, "base64url");
|
|
1011
|
+
function endpoints(cfg) {
|
|
1012
|
+
const base = cfg.issuer.replace(/\/+$/, "");
|
|
1013
|
+
return {
|
|
1014
|
+
authorize: cfg.authorizeUrl ?? `${base}/api/oauth2/authorize`,
|
|
1015
|
+
token: cfg.tokenUrl ?? `${base}/api/oauth2/token`,
|
|
1016
|
+
device: cfg.deviceAuthorizationUrl ?? `${base}/api/oauth2/device_authorization`
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
__name(endpoints, "endpoints");
|
|
1020
|
+
var DEFAULT_SCOPES = [
|
|
1021
|
+
"openid",
|
|
1022
|
+
"profile",
|
|
1023
|
+
"email",
|
|
1024
|
+
"ledger"
|
|
1025
|
+
];
|
|
1026
|
+
function buildAuthorizeUrl(cfg, args) {
|
|
1027
|
+
const redirectUri = args.redirectUri ?? cfg.redirectUri;
|
|
1028
|
+
if (!redirectUri) {
|
|
1029
|
+
throw new AccountError({
|
|
1030
|
+
code: "BAD_REQUEST",
|
|
1031
|
+
message: "redirectUri required for authorize"
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const url = new URL(endpoints(cfg).authorize);
|
|
1035
|
+
url.searchParams.set("response_type", "code");
|
|
1036
|
+
url.searchParams.set("client_id", cfg.clientId);
|
|
1037
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
1038
|
+
url.searchParams.set("scope", (args.scopes ?? cfg.scopes ?? DEFAULT_SCOPES).join(" "));
|
|
1039
|
+
url.searchParams.set("state", args.state);
|
|
1040
|
+
url.searchParams.set("code_challenge", args.codeChallenge);
|
|
1041
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
1042
|
+
if (args.nonce) url.searchParams.set("nonce", args.nonce);
|
|
1043
|
+
if (args.prompt) url.searchParams.set("prompt", args.prompt);
|
|
1044
|
+
if (args.maxAge !== void 0) url.searchParams.set("max_age", String(args.maxAge));
|
|
1045
|
+
if (args.acrValues) url.searchParams.set("acr_values", args.acrValues);
|
|
1046
|
+
return url.toString();
|
|
1047
|
+
}
|
|
1048
|
+
__name(buildAuthorizeUrl, "buildAuthorizeUrl");
|
|
1049
|
+
async function exchangeCode(cfg, transport, args) {
|
|
1050
|
+
const redirectUri = args.redirectUri ?? cfg.redirectUri;
|
|
1051
|
+
if (!redirectUri) {
|
|
1052
|
+
throw new AccountError({
|
|
1053
|
+
code: "BAD_REQUEST",
|
|
1054
|
+
message: "redirectUri required for code exchange"
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
const body = new URLSearchParams({
|
|
1058
|
+
grant_type: "authorization_code",
|
|
1059
|
+
code: args.code,
|
|
1060
|
+
redirect_uri: redirectUri,
|
|
1061
|
+
client_id: cfg.clientId,
|
|
1062
|
+
code_verifier: args.codeVerifier
|
|
1063
|
+
});
|
|
1064
|
+
if (cfg.clientSecret) body.set("client_secret", cfg.clientSecret);
|
|
1065
|
+
return postToken(cfg, transport, body, {
|
|
1066
|
+
requireIdToken: true,
|
|
1067
|
+
expectedNonce: args.expectedNonce,
|
|
1068
|
+
expectedScopes: args.expectedScopes,
|
|
1069
|
+
signal: args.signal
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
__name(exchangeCode, "exchangeCode");
|
|
1073
|
+
async function refreshTokens(cfg, transport, refreshToken) {
|
|
1074
|
+
const body = new URLSearchParams({
|
|
1075
|
+
grant_type: "refresh_token",
|
|
1076
|
+
refresh_token: refreshToken,
|
|
1077
|
+
client_id: cfg.clientId
|
|
1078
|
+
});
|
|
1079
|
+
if (cfg.clientSecret) body.set("client_secret", cfg.clientSecret);
|
|
1080
|
+
return postToken(cfg, transport, body);
|
|
1081
|
+
}
|
|
1082
|
+
__name(refreshTokens, "refreshTokens");
|
|
1083
|
+
async function startDeviceAuthorization(cfg, transport, scopes) {
|
|
1084
|
+
const body = new URLSearchParams({
|
|
1085
|
+
client_id: cfg.clientId,
|
|
1086
|
+
scope: (scopes ?? cfg.scopes ?? DEFAULT_SCOPES).join(" ")
|
|
1087
|
+
});
|
|
1088
|
+
const raw = await transport.request({
|
|
1089
|
+
method: "POST",
|
|
1090
|
+
url: endpoints(cfg).device,
|
|
1091
|
+
headers: {
|
|
1092
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
1093
|
+
},
|
|
1094
|
+
body: body.toString()
|
|
1095
|
+
});
|
|
1096
|
+
const expiresIn = Number(raw["expires_in"] ?? 600);
|
|
1097
|
+
return {
|
|
1098
|
+
deviceCode: String(raw["device_code"] ?? ""),
|
|
1099
|
+
userCode: String(raw["user_code"] ?? ""),
|
|
1100
|
+
verificationUri: String(raw["verification_uri"] ?? ""),
|
|
1101
|
+
verificationUriComplete: typeof raw["verification_uri_complete"] === "string" ? raw["verification_uri_complete"] : null,
|
|
1102
|
+
expiresAt: new Date(Date.now() + expiresIn * 1e3),
|
|
1103
|
+
intervalSeconds: Number(raw["interval"] ?? 5)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
__name(startDeviceAuthorization, "startDeviceAuthorization");
|
|
1107
|
+
async function pollDeviceToken(cfg, transport, device, opts = {}) {
|
|
1108
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => {
|
|
1109
|
+
const signal = opts.signal;
|
|
1110
|
+
const timer = setTimeout(done, ms);
|
|
1111
|
+
function done() {
|
|
1112
|
+
clearTimeout(timer);
|
|
1113
|
+
signal?.removeEventListener("abort", done);
|
|
1114
|
+
resolve();
|
|
1115
|
+
}
|
|
1116
|
+
__name(done, "done");
|
|
1117
|
+
signal?.addEventListener("abort", done, {
|
|
1118
|
+
once: true
|
|
1119
|
+
});
|
|
1120
|
+
if (signal?.aborted) done();
|
|
1121
|
+
}));
|
|
1122
|
+
let intervalMs = device.intervalSeconds * 1e3;
|
|
1123
|
+
while (Date.now() < device.expiresAt.getTime()) {
|
|
1124
|
+
if (opts.signal?.aborted) {
|
|
1125
|
+
throw new AccountError({
|
|
1126
|
+
code: "BAD_REQUEST",
|
|
1127
|
+
message: "device authorization aborted"
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
await sleep2(intervalMs);
|
|
1131
|
+
if (opts.signal?.aborted) {
|
|
1132
|
+
throw new AccountError({
|
|
1133
|
+
code: "BAD_REQUEST",
|
|
1134
|
+
message: "device authorization aborted"
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
if (Date.now() >= device.expiresAt.getTime()) break;
|
|
1138
|
+
const body = new URLSearchParams({
|
|
1139
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1140
|
+
device_code: device.deviceCode,
|
|
1141
|
+
client_id: cfg.clientId
|
|
1142
|
+
});
|
|
1143
|
+
try {
|
|
1144
|
+
const tokens = await postToken(cfg, transport, body, {
|
|
1145
|
+
requireIdToken: true,
|
|
1146
|
+
signal: opts.signal
|
|
1147
|
+
});
|
|
1148
|
+
if (opts.signal?.aborted) throw new AccountError({
|
|
1149
|
+
code: "BAD_REQUEST",
|
|
1150
|
+
message: "device authorization aborted"
|
|
1151
|
+
});
|
|
1152
|
+
return tokens;
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
if (!(err instanceof AccountError)) throw err;
|
|
1155
|
+
const oauthErr = err.oauthError;
|
|
1156
|
+
if (err.httpStatus === 400 && oauthErr === "authorization_pending") continue;
|
|
1157
|
+
if (err.httpStatus === 400 && oauthErr === "slow_down") {
|
|
1158
|
+
intervalMs += 5e3;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
throw err;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
throw new AccountError({
|
|
1165
|
+
code: "BAD_REQUEST",
|
|
1166
|
+
message: "device code expired before approval"
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
__name(pollDeviceToken, "pollDeviceToken");
|
|
1170
|
+
async function postToken(cfg, transport, body, verify = {}) {
|
|
1171
|
+
const tokenUrl = endpoints(cfg).token;
|
|
1172
|
+
const dpop = await cfg.dpopSigner.createProof({
|
|
1173
|
+
method: "POST",
|
|
1174
|
+
url: tokenUrl
|
|
1175
|
+
});
|
|
1176
|
+
if (verify.signal?.aborted) throw new AccountError({
|
|
1177
|
+
code: "BAD_REQUEST",
|
|
1178
|
+
message: "Account sign-in was cancelled",
|
|
1179
|
+
retryable: false
|
|
1180
|
+
});
|
|
1181
|
+
const raw = await transport.request({
|
|
1182
|
+
method: "POST",
|
|
1183
|
+
url: tokenUrl,
|
|
1184
|
+
headers: {
|
|
1185
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1186
|
+
dpop
|
|
1187
|
+
},
|
|
1188
|
+
body: body.toString(),
|
|
1189
|
+
oauthErrorResponse: true,
|
|
1190
|
+
signal: verify.signal
|
|
1191
|
+
});
|
|
1192
|
+
const parsed = oauthTokenResponseSchema.safeParse(raw);
|
|
1193
|
+
if (!parsed.success) {
|
|
1194
|
+
throw new AccountError({
|
|
1195
|
+
code: "INVALID_RESPONSE",
|
|
1196
|
+
message: "token endpoint returned an unrecognised shape"
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
const t = parsed.data;
|
|
1200
|
+
if (t.token_type.toLowerCase() !== "dpop") throw new AccountError({
|
|
1201
|
+
code: "INVALID_RESPONSE",
|
|
1202
|
+
message: "token endpoint did not issue a DPoP token"
|
|
1203
|
+
});
|
|
1204
|
+
if (verify.requireIdToken && !t.id_token) throw new AccountError({
|
|
1205
|
+
code: "INVALID_RESPONSE",
|
|
1206
|
+
message: "token endpoint omitted id_token"
|
|
1207
|
+
});
|
|
1208
|
+
const accessClaims = await verifyAccessToken({
|
|
1209
|
+
token: t.access_token,
|
|
1210
|
+
transport,
|
|
1211
|
+
issuer: cfg.issuer,
|
|
1212
|
+
clientId: cfg.clientId,
|
|
1213
|
+
dpopJkt: cfg.dpopSigner.jkt,
|
|
1214
|
+
jwksUrl: cfg.jwksUrl,
|
|
1215
|
+
expectedScopes: verify.expectedScopes
|
|
1216
|
+
});
|
|
1217
|
+
const idClaims = t.id_token ? await verifyIdToken({
|
|
1218
|
+
token: t.id_token,
|
|
1219
|
+
transport,
|
|
1220
|
+
issuer: cfg.issuer,
|
|
1221
|
+
clientId: cfg.clientId,
|
|
1222
|
+
jwksUrl: cfg.jwksUrl,
|
|
1223
|
+
expectedNonce: verify.expectedNonce,
|
|
1224
|
+
expectedSubject: accessClaims.sub
|
|
1225
|
+
}) : null;
|
|
1226
|
+
return tokenSetSchema.parse({
|
|
1227
|
+
accessToken: t.access_token,
|
|
1228
|
+
tokenType: t.token_type,
|
|
1229
|
+
expiresAt: t.expires_in ? new Date(Date.now() + t.expires_in * 1e3) : null,
|
|
1230
|
+
refreshToken: t.refresh_token ?? null,
|
|
1231
|
+
idToken: t.id_token ?? null,
|
|
1232
|
+
scope: t.scope ? t.scope.split(/\s+/).filter(Boolean) : [],
|
|
1233
|
+
sid: idClaims?.sid ?? accessClaims.sid,
|
|
1234
|
+
accessClaims,
|
|
1235
|
+
idClaims
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
__name(postToken, "postToken");
|
|
1239
|
+
|
|
1240
|
+
// src/tokenStore.ts
|
|
1241
|
+
function memoryStore() {
|
|
1242
|
+
const m = /* @__PURE__ */ new Map();
|
|
1243
|
+
const locks = /* @__PURE__ */ new Map();
|
|
1244
|
+
return {
|
|
1245
|
+
async withExclusive(key, work) {
|
|
1246
|
+
const prior = locks.get(key) ?? Promise.resolve();
|
|
1247
|
+
let release;
|
|
1248
|
+
const held = new Promise((resolve) => {
|
|
1249
|
+
release = resolve;
|
|
1250
|
+
});
|
|
1251
|
+
const tail = prior.then(() => held);
|
|
1252
|
+
locks.set(key, tail);
|
|
1253
|
+
await prior;
|
|
1254
|
+
try {
|
|
1255
|
+
return await work();
|
|
1256
|
+
} finally {
|
|
1257
|
+
release();
|
|
1258
|
+
if (locks.get(key) === tail) locks.delete(key);
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1261
|
+
async get(k) {
|
|
1262
|
+
return m.get(k) ?? null;
|
|
1263
|
+
},
|
|
1264
|
+
async set(k, v) {
|
|
1265
|
+
m.set(k, v);
|
|
1266
|
+
},
|
|
1267
|
+
async delete(k) {
|
|
1268
|
+
m.delete(k);
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
__name(memoryStore, "memoryStore");
|
|
1273
|
+
function keychainStore(kc) {
|
|
1274
|
+
return {
|
|
1275
|
+
...kc.withExclusive ? {
|
|
1276
|
+
withExclusive: /* @__PURE__ */ __name((key, work) => kc.withExclusive(key, work), "withExclusive")
|
|
1277
|
+
} : {},
|
|
1278
|
+
async get(k) {
|
|
1279
|
+
return await kc.get(k) ?? null;
|
|
1280
|
+
},
|
|
1281
|
+
async set(k, v) {
|
|
1282
|
+
await kc.set(k, v);
|
|
1283
|
+
},
|
|
1284
|
+
async delete(k) {
|
|
1285
|
+
await kc.delete(k);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
__name(keychainStore, "keychainStore");
|
|
1290
|
+
|
|
1291
|
+
// src/client.ts
|
|
1292
|
+
var XenoAccountClient = class {
|
|
1293
|
+
static {
|
|
1294
|
+
__name(this, "XenoAccountClient");
|
|
1295
|
+
}
|
|
1296
|
+
surface;
|
|
1297
|
+
account;
|
|
1298
|
+
ledger;
|
|
1299
|
+
authz;
|
|
1300
|
+
oidcConfig;
|
|
1301
|
+
transport;
|
|
1302
|
+
store;
|
|
1303
|
+
apiKey;
|
|
1304
|
+
broker;
|
|
1305
|
+
refreshStoreKey;
|
|
1306
|
+
refreshPendingKey;
|
|
1307
|
+
access = null;
|
|
1308
|
+
user = null;
|
|
1309
|
+
interactive;
|
|
1310
|
+
resolutionFlight = null;
|
|
1311
|
+
generation = 0;
|
|
1312
|
+
locallySignedOut = false;
|
|
1313
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1314
|
+
constructor(cfg) {
|
|
1315
|
+
this.surface = surfaceSchema.parse(cfg.surface);
|
|
1316
|
+
this.transport = new Transport(cfg.transport);
|
|
1317
|
+
this.oidcConfig = {
|
|
1318
|
+
issuer: cfg.issuer,
|
|
1319
|
+
...cfg.oidc
|
|
1320
|
+
};
|
|
1321
|
+
this.refreshStoreKey = `xeno.account/${this.oidcConfig.clientId}/refresh`;
|
|
1322
|
+
this.refreshPendingKey = `${this.refreshStoreKey}/pending`;
|
|
1323
|
+
this.account = new AccountClient(cfg.issuer, this.transport);
|
|
1324
|
+
this.ledger = new LedgerClient(cfg.apiBase, this.transport);
|
|
1325
|
+
this.authz = new AuthzClient(cfg.apiBase, this.transport);
|
|
1326
|
+
this.store = cfg.store ?? memoryStore();
|
|
1327
|
+
this.apiKey = cfg.apiKey ?? (typeof process !== "undefined" ? process.env?.XENO_API_KEY : void 0) ?? void 0;
|
|
1328
|
+
this.broker = cfg.broker;
|
|
1329
|
+
}
|
|
1330
|
+
// ─── Session workhorse (priority chain, XENO AUTH §5) ───────────────────
|
|
1331
|
+
/**
|
|
1332
|
+
* Return a valid Bearer for the gate, walking the chain:
|
|
1333
|
+
* 1 in-memory access token → 2 explicit override (apiKey/XENO_API_KEY) →
|
|
1334
|
+
* 3 [broker — host-wired] → 4 this product's own refresh token →
|
|
1335
|
+
* 5 interactive sign-in (host-provided; skipped when `silent`) → null.
|
|
1336
|
+
*/
|
|
1337
|
+
async getAccessToken(opts = {}) {
|
|
1338
|
+
const generation = this.generation;
|
|
1339
|
+
const scopes = opts.scopes ? [
|
|
1340
|
+
...opts.scopes
|
|
1341
|
+
] : void 0;
|
|
1342
|
+
if (this.access && this.access.expiresAtMs - 6e4 > Date.now() && containsScopes(this.access.scopes, scopes)) {
|
|
1343
|
+
const held = this.access;
|
|
1344
|
+
if (await this.cacheCurrent(held) && this.generation === generation) return held.token;
|
|
1345
|
+
}
|
|
1346
|
+
if (this.apiKey) return this.apiKey;
|
|
1347
|
+
if (this.locallySignedOut && opts.silent) return null;
|
|
1348
|
+
if (!this.resolutionFlight) {
|
|
1349
|
+
const flight = this.resolveSession({
|
|
1350
|
+
scopes,
|
|
1351
|
+
silent: opts.silent
|
|
1352
|
+
}, generation);
|
|
1353
|
+
this.resolutionFlight = flight;
|
|
1354
|
+
void flight.finally(() => {
|
|
1355
|
+
if (this.resolutionFlight === flight) this.resolutionFlight = null;
|
|
1356
|
+
}).catch(() => {
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
await this.resolutionFlight;
|
|
1360
|
+
if (this.generation !== generation || !this.access) return null;
|
|
1361
|
+
if (!await this.cacheCurrent(this.access) || this.generation !== generation || !this.access) return null;
|
|
1362
|
+
if (!containsScopes(this.access.scopes, scopes)) throw new AccountError({
|
|
1363
|
+
code: "INVALID_TOKEN",
|
|
1364
|
+
message: "Current account session lacks required scope",
|
|
1365
|
+
retryable: false
|
|
1366
|
+
});
|
|
1367
|
+
if (this.access.expiresAtMs <= Date.now()) return null;
|
|
1368
|
+
return this.access.token;
|
|
1369
|
+
}
|
|
1370
|
+
async resolveSession(opts, generation) {
|
|
1371
|
+
if (this.locallySignedOut) {
|
|
1372
|
+
if (!opts.silent && this.interactive) await this.resolveInteractive(opts.scopes, generation);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const pending = await this.storeRead(this.refreshPendingKey);
|
|
1376
|
+
if (this.generation !== generation) return;
|
|
1377
|
+
if (pending && !/^ready:v1:[A-Za-z0-9_-]{32}$/.test(pending)) {
|
|
1378
|
+
if (!opts.silent && this.interactive) {
|
|
1379
|
+
await this.resolveInteractive(opts.scopes, generation);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (pending === "signed-out:v1") {
|
|
1383
|
+
this.setUnauthenticated();
|
|
1384
|
+
if (!await this.storeRead(this.refreshStoreKey)) return;
|
|
1385
|
+
} else {
|
|
1386
|
+
throw new AccountError({
|
|
1387
|
+
code: "CONFLICT",
|
|
1388
|
+
message: "Previous account transition is unconfirmed; explicit sign-in recovery required",
|
|
1389
|
+
retryable: false
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (pending === "signed-out:v1") throw transitionConflict();
|
|
1393
|
+
}
|
|
1394
|
+
if (this.broker) {
|
|
1395
|
+
try {
|
|
1396
|
+
const brokerToken = await this.broker.getToken({
|
|
1397
|
+
clientId: this.oidcConfig.clientId,
|
|
1398
|
+
scopes: opts.scopes ?? this.oidcConfig.scopes ?? [],
|
|
1399
|
+
resource: "https://api.xenostudio.ai",
|
|
1400
|
+
childDpopJkt: this.oidcConfig.dpopSigner.jkt
|
|
1401
|
+
});
|
|
1402
|
+
if (brokerToken) {
|
|
1403
|
+
const claims = await verifyAccessToken({
|
|
1404
|
+
token: brokerToken.accessToken,
|
|
1405
|
+
transport: this.transport,
|
|
1406
|
+
issuer: this.oidcConfig.issuer,
|
|
1407
|
+
clientId: this.oidcConfig.clientId,
|
|
1408
|
+
dpopJkt: this.oidcConfig.dpopSigner.jkt,
|
|
1409
|
+
jwksUrl: this.oidcConfig.jwksUrl,
|
|
1410
|
+
expectedScopes: opts.scopes,
|
|
1411
|
+
requireBrokerActor: true
|
|
1412
|
+
});
|
|
1413
|
+
if (brokerToken.refreshToken) throw new AccountError({
|
|
1414
|
+
code: "INVALID_RESPONSE",
|
|
1415
|
+
message: "Broker cannot transfer refresh credentials",
|
|
1416
|
+
retryable: false
|
|
1417
|
+
});
|
|
1418
|
+
await this.applyTokenSet({
|
|
1419
|
+
...brokerToken,
|
|
1420
|
+
accessClaims: claims,
|
|
1421
|
+
idClaims: null
|
|
1422
|
+
}, generation, pending, true);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (this.generation !== generation) return;
|
|
1429
|
+
const transition = `refresh:v1:${generateState()}`;
|
|
1430
|
+
const rt = await this.exclusiveStore(async () => {
|
|
1431
|
+
if (this.generation !== generation) return null;
|
|
1432
|
+
if (await this.store.get(this.refreshPendingKey) !== pending) throw transitionConflict();
|
|
1433
|
+
const value = await this.store.get(this.refreshStoreKey);
|
|
1434
|
+
if (value) await this.store.set(this.refreshPendingKey, transition);
|
|
1435
|
+
return value;
|
|
1436
|
+
});
|
|
1437
|
+
if (this.generation !== generation) return;
|
|
1438
|
+
if (rt) {
|
|
1439
|
+
try {
|
|
1440
|
+
const tokens = await this.refresh(rt);
|
|
1441
|
+
if (!tokens.refreshToken) throw new AccountError({
|
|
1442
|
+
code: "INVALID_RESPONSE",
|
|
1443
|
+
message: "Rotating refresh response omitted successor credential",
|
|
1444
|
+
retryable: false
|
|
1445
|
+
});
|
|
1446
|
+
await this.applyTokenSet(tokens, generation, transition);
|
|
1447
|
+
return;
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
if (this.generation !== generation) return;
|
|
1450
|
+
if (!(error instanceof AccountError && error.httpStatus === 400 && error.oauthError === "invalid_grant")) {
|
|
1451
|
+
throw new AccountError({
|
|
1452
|
+
code: error instanceof AccountError ? error.code : "UNREACHABLE",
|
|
1453
|
+
message: "Account refresh outcome is unconfirmed; explicit recovery required",
|
|
1454
|
+
retryable: false,
|
|
1455
|
+
...error instanceof AccountError ? {
|
|
1456
|
+
httpStatus: error.httpStatus
|
|
1457
|
+
} : {}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
await this.exclusiveStore(async () => {
|
|
1461
|
+
if (this.generation !== generation) return;
|
|
1462
|
+
if (await this.store.get(this.refreshPendingKey) !== transition) throw transitionConflict();
|
|
1463
|
+
await this.store.delete(this.refreshStoreKey);
|
|
1464
|
+
await this.store.set(this.refreshPendingKey, "signed-out:v1");
|
|
1465
|
+
});
|
|
1466
|
+
if (this.generation !== generation) return;
|
|
1467
|
+
this.setUnauthenticated();
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (!opts.silent && this.interactive) {
|
|
1471
|
+
await this.resolveInteractive(opts.scopes, generation);
|
|
1472
|
+
}
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
/** Register the host's interactive sign-in (loopback PKCE / device grant). */
|
|
1476
|
+
setInteractiveSignIn(fn) {
|
|
1477
|
+
this.interactive = fn;
|
|
1478
|
+
}
|
|
1479
|
+
isAuthenticated() {
|
|
1480
|
+
return !!this.access && this.access.expiresAtMs > Date.now();
|
|
1481
|
+
}
|
|
1482
|
+
currentUser() {
|
|
1483
|
+
return this.user;
|
|
1484
|
+
}
|
|
1485
|
+
/** Subscribe to login/refresh/logout/backchannel. Returns an unsubscribe fn. */
|
|
1486
|
+
onAuthChanged(cb) {
|
|
1487
|
+
this.listeners.add(cb);
|
|
1488
|
+
return () => this.listeners.delete(cb);
|
|
1489
|
+
}
|
|
1490
|
+
/** Local wipe only, used after durable host-side invalidation/reconciliation. */
|
|
1491
|
+
async signOutLocal() {
|
|
1492
|
+
this.generation++;
|
|
1493
|
+
this.locallySignedOut = true;
|
|
1494
|
+
this.resolutionFlight = null;
|
|
1495
|
+
this.setUnauthenticated();
|
|
1496
|
+
await this.exclusiveStore(async () => {
|
|
1497
|
+
await this.store.set(this.refreshPendingKey, "signed-out:v1");
|
|
1498
|
+
await this.store.delete(this.refreshStoreKey);
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
/** Revoke this subject-owned session, then always clear local state. */
|
|
1502
|
+
async signOut() {
|
|
1503
|
+
const original = this.access;
|
|
1504
|
+
const local = this.signOutLocal();
|
|
1505
|
+
void local.catch(() => {
|
|
1506
|
+
});
|
|
1507
|
+
let serverRevoked = false;
|
|
1508
|
+
try {
|
|
1509
|
+
if (original?.tokenType === "DPoP") {
|
|
1510
|
+
const url = `${this.oidcConfig.issuer.replace(/\/+$/, "")}/api/oauth2/end_session`;
|
|
1511
|
+
const dpop = await this.createProof({
|
|
1512
|
+
method: "POST",
|
|
1513
|
+
url,
|
|
1514
|
+
accessToken: original.token
|
|
1515
|
+
});
|
|
1516
|
+
await this.transport.request({
|
|
1517
|
+
method: "POST",
|
|
1518
|
+
url,
|
|
1519
|
+
token: original.token,
|
|
1520
|
+
tokenType: "DPoP",
|
|
1521
|
+
headers: {
|
|
1522
|
+
dpop
|
|
1523
|
+
},
|
|
1524
|
+
idempotencyKey: `logout:${original.sid ?? "current"}`
|
|
1525
|
+
});
|
|
1526
|
+
serverRevoked = true;
|
|
1527
|
+
}
|
|
1528
|
+
} finally {
|
|
1529
|
+
await local;
|
|
1530
|
+
}
|
|
1531
|
+
return {
|
|
1532
|
+
serverRevoked
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
/** Step-up protected, subject-keyed global logout. Only authorized clients can call it. */
|
|
1536
|
+
async signOutEverywhere() {
|
|
1537
|
+
if (!this.access || this.access.tokenType !== "DPoP") throw new Error("fresh DPoP session required");
|
|
1538
|
+
const original = this.access, generation = this.generation;
|
|
1539
|
+
const url = `${this.oidcConfig.issuer.replace(/\/+$/, "")}/api/oauth2/logout_everywhere`;
|
|
1540
|
+
const dpop = await this.createProof({
|
|
1541
|
+
method: "POST",
|
|
1542
|
+
url,
|
|
1543
|
+
accessToken: original.token
|
|
1544
|
+
});
|
|
1545
|
+
if (this.generation !== generation || this.access !== original) throw new AccountError({
|
|
1546
|
+
code: "CONFLICT",
|
|
1547
|
+
message: "Account session changed before global revocation",
|
|
1548
|
+
retryable: false
|
|
1549
|
+
});
|
|
1550
|
+
await this.transport.request({
|
|
1551
|
+
method: "POST",
|
|
1552
|
+
url,
|
|
1553
|
+
token: original.token,
|
|
1554
|
+
tokenType: "DPoP",
|
|
1555
|
+
headers: {
|
|
1556
|
+
dpop
|
|
1557
|
+
},
|
|
1558
|
+
idempotencyKey: `logout-all:${original.sid ?? "current"}`
|
|
1559
|
+
});
|
|
1560
|
+
try {
|
|
1561
|
+
await this.broker?.logout?.();
|
|
1562
|
+
} finally {
|
|
1563
|
+
if (this.generation === generation) {
|
|
1564
|
+
this.generation++;
|
|
1565
|
+
this.locallySignedOut = true;
|
|
1566
|
+
this.resolutionFlight = null;
|
|
1567
|
+
this.setUnauthenticated();
|
|
1568
|
+
await this.exclusiveStore(async () => {
|
|
1569
|
+
if (await this.store.get(this.refreshPendingKey) !== original.storeTransition) return;
|
|
1570
|
+
await this.store.set(this.refreshPendingKey, "signed-out:v1");
|
|
1571
|
+
await this.store.delete(this.refreshStoreKey);
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
setUnauthenticated() {
|
|
1577
|
+
this.access = null;
|
|
1578
|
+
this.user = null;
|
|
1579
|
+
this.emit(null);
|
|
1580
|
+
}
|
|
1581
|
+
emit(a) {
|
|
1582
|
+
for (const cb of [
|
|
1583
|
+
...this.listeners
|
|
1584
|
+
]) {
|
|
1585
|
+
try {
|
|
1586
|
+
cb(a);
|
|
1587
|
+
} catch {
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
async storeRead(key) {
|
|
1592
|
+
try {
|
|
1593
|
+
return await this.store.get(key);
|
|
1594
|
+
} catch {
|
|
1595
|
+
throw new AccountError({
|
|
1596
|
+
code: "UNREACHABLE",
|
|
1597
|
+
message: "Secure account store is unavailable",
|
|
1598
|
+
retryable: false
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async cacheCurrent(held) {
|
|
1603
|
+
const transition = await this.storeRead(this.refreshPendingKey);
|
|
1604
|
+
if (this.access === held && transition === held.storeTransition) return true;
|
|
1605
|
+
if (this.access === held) this.setUnauthenticated();
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
async exclusiveStore(work) {
|
|
1609
|
+
if (!this.store.withExclusive) throw new AccountError({
|
|
1610
|
+
code: "UNREACHABLE",
|
|
1611
|
+
message: "Secure account store exclusion is unavailable",
|
|
1612
|
+
retryable: false
|
|
1613
|
+
});
|
|
1614
|
+
try {
|
|
1615
|
+
return await this.store.withExclusive(this.refreshStoreKey, work);
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
if (error instanceof AccountError) throw error;
|
|
1618
|
+
throw new AccountError({
|
|
1619
|
+
code: "UNREACHABLE",
|
|
1620
|
+
message: "Secure account state commit is unavailable",
|
|
1621
|
+
retryable: false
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async reserveSignIn(generation) {
|
|
1626
|
+
const transition = `sign-in:v1:${generateState()}`;
|
|
1627
|
+
await this.exclusiveStore(async () => {
|
|
1628
|
+
if (this.generation !== generation) throw transitionConflict();
|
|
1629
|
+
await this.store.set(this.refreshPendingKey, transition);
|
|
1630
|
+
});
|
|
1631
|
+
return transition;
|
|
1632
|
+
}
|
|
1633
|
+
async resolveInteractive(scopes, generation) {
|
|
1634
|
+
const transition = await this.reserveSignIn(generation);
|
|
1635
|
+
await this.applyTokenSet(await this.interactive(scopes), generation, transition);
|
|
1636
|
+
}
|
|
1637
|
+
/** Cache the access token, persist the rotated refresh token, update the user. */
|
|
1638
|
+
async applyTokenSet(ts, generation, transition, preserveRefresh = false, signal) {
|
|
1639
|
+
if (this.generation !== generation) return;
|
|
1640
|
+
const claims = await verifyAccessToken({
|
|
1641
|
+
token: ts.accessToken,
|
|
1642
|
+
transport: this.transport,
|
|
1643
|
+
issuer: this.oidcConfig.issuer,
|
|
1644
|
+
clientId: this.oidcConfig.clientId,
|
|
1645
|
+
dpopJkt: this.oidcConfig.dpopSigner.jkt,
|
|
1646
|
+
jwksUrl: this.oidcConfig.jwksUrl
|
|
1647
|
+
});
|
|
1648
|
+
const idClaims = ts.idToken ? await verifyIdToken({
|
|
1649
|
+
token: ts.idToken,
|
|
1650
|
+
transport: this.transport,
|
|
1651
|
+
issuer: this.oidcConfig.issuer,
|
|
1652
|
+
clientId: this.oidcConfig.clientId,
|
|
1653
|
+
jwksUrl: this.oidcConfig.jwksUrl,
|
|
1654
|
+
expectedSubject: claims.sub
|
|
1655
|
+
}) : null;
|
|
1656
|
+
if (idClaims && idClaims.sid !== claims.sid) throw new AccountError({
|
|
1657
|
+
code: "INVALID_RESPONSE",
|
|
1658
|
+
message: "ID and access session binding differ",
|
|
1659
|
+
retryable: false
|
|
1660
|
+
});
|
|
1661
|
+
if (ts.tokenType.toLowerCase() !== "dpop") throw new AccountError({
|
|
1662
|
+
code: "INVALID_RESPONSE",
|
|
1663
|
+
message: "Canonical session requires DPoP token type",
|
|
1664
|
+
retryable: false
|
|
1665
|
+
});
|
|
1666
|
+
if (this.generation !== generation) return;
|
|
1667
|
+
const expMs = Math.min(claims.exp * 1e3, ts.expiresAt?.getTime() ?? claims.exp * 1e3);
|
|
1668
|
+
const storeTransition = `ready:v1:${generateState()}`;
|
|
1669
|
+
await this.exclusiveStore(async () => {
|
|
1670
|
+
if (this.generation !== generation) return;
|
|
1671
|
+
if (await this.store.get(this.refreshPendingKey) !== transition) throw transitionConflict();
|
|
1672
|
+
if (ts.refreshToken) {
|
|
1673
|
+
await this.store.set(this.refreshStoreKey, ts.refreshToken);
|
|
1674
|
+
} else if (!preserveRefresh) await this.store.delete(this.refreshStoreKey);
|
|
1675
|
+
if (this.generation !== generation || signal?.aborted) throw transitionConflict();
|
|
1676
|
+
await this.store.set(this.refreshPendingKey, storeTransition);
|
|
1677
|
+
if (signal?.aborted) {
|
|
1678
|
+
if (await this.store.get(this.refreshPendingKey) === storeTransition) {
|
|
1679
|
+
await this.store.set(this.refreshPendingKey, "signed-out:v1");
|
|
1680
|
+
await this.store.delete(this.refreshStoreKey);
|
|
1681
|
+
}
|
|
1682
|
+
throw transitionConflict();
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
if (this.generation !== generation) {
|
|
1686
|
+
if (signal?.aborted) await this.cancelCommittedSignIn(storeTransition);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
this.access = {
|
|
1690
|
+
token: ts.accessToken,
|
|
1691
|
+
tokenType: ts.tokenType.toLowerCase() === "dpop" ? "DPoP" : "Bearer",
|
|
1692
|
+
expiresAtMs: expMs,
|
|
1693
|
+
scopes: claims.scope.split(/\s+/).filter(Boolean),
|
|
1694
|
+
sid: claims.sid,
|
|
1695
|
+
storeTransition
|
|
1696
|
+
};
|
|
1697
|
+
this.locallySignedOut = false;
|
|
1698
|
+
if (idClaims) {
|
|
1699
|
+
const u = accountFromVerifiedClaims(idClaims);
|
|
1700
|
+
if (u) this.user = u;
|
|
1701
|
+
} else if (this.user?.id !== claims.sub) this.user = null;
|
|
1702
|
+
this.emit(this.user);
|
|
1703
|
+
return storeTransition;
|
|
1704
|
+
}
|
|
1705
|
+
async cancelCommittedSignIn(transition) {
|
|
1706
|
+
await this.exclusiveStore(async () => {
|
|
1707
|
+
if (await this.store.get(this.refreshPendingKey) !== transition) return;
|
|
1708
|
+
await this.store.set(this.refreshPendingKey, "signed-out:v1");
|
|
1709
|
+
await this.store.delete(this.refreshStoreKey);
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
// ─── OIDC sign-in (Authorization Code + PKCE) ───────────────────────────
|
|
1713
|
+
/**
|
|
1714
|
+
* Begin "Continue with XENO". Returns the authorize URL to redirect to, plus
|
|
1715
|
+
* the `{ state, verifier }` the caller MUST persist (encrypted) to complete
|
|
1716
|
+
* the flow in {@link completeSignIn}.
|
|
1717
|
+
*/
|
|
1718
|
+
async beginSignIn(args = {}) {
|
|
1719
|
+
const state = generateState();
|
|
1720
|
+
const nonce = generateState();
|
|
1721
|
+
const pkce = await generatePkce();
|
|
1722
|
+
const url = buildAuthorizeUrl(this.oidcConfig, {
|
|
1723
|
+
state,
|
|
1724
|
+
codeChallenge: pkce.challenge,
|
|
1725
|
+
redirectUri: args.redirectUri,
|
|
1726
|
+
scopes: args.scopes,
|
|
1727
|
+
nonce,
|
|
1728
|
+
...args.fresh ? {
|
|
1729
|
+
prompt: "login",
|
|
1730
|
+
maxAge: 60,
|
|
1731
|
+
acrValues: "urn:xeno:acr:fresh"
|
|
1732
|
+
} : {}
|
|
1733
|
+
});
|
|
1734
|
+
return {
|
|
1735
|
+
url,
|
|
1736
|
+
state,
|
|
1737
|
+
nonce,
|
|
1738
|
+
pkce
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
/** Complete the code flow: exchange `code` (with the saved verifier) for tokens,
|
|
1742
|
+
* then cache the access token + persist the refresh token + set the user. */
|
|
1743
|
+
async completeSignIn(args) {
|
|
1744
|
+
const generation = ++this.generation;
|
|
1745
|
+
this.resolutionFlight = null;
|
|
1746
|
+
const invalidate = this.bindSignInCancellation(generation, args.signal);
|
|
1747
|
+
try {
|
|
1748
|
+
if (args.signal?.aborted) throw transitionConflict();
|
|
1749
|
+
const transition = await this.reserveSignIn(generation);
|
|
1750
|
+
if (args.signal?.aborted || this.generation !== generation) throw transitionConflict();
|
|
1751
|
+
const ts = await exchangeCode(this.oidcConfig, this.transport, args);
|
|
1752
|
+
const committed = await this.applyTokenSet(ts, generation, transition, false, args.signal);
|
|
1753
|
+
if (this.generation !== generation || args.signal?.aborted) {
|
|
1754
|
+
if (committed && args.signal?.aborted) await this.cancelCommittedSignIn(committed);
|
|
1755
|
+
throw transitionConflict();
|
|
1756
|
+
}
|
|
1757
|
+
return ts;
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (args.signal?.aborted) throw transitionConflict();
|
|
1760
|
+
throw error;
|
|
1761
|
+
} finally {
|
|
1762
|
+
invalidate();
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
bindSignInCancellation(generation, signal) {
|
|
1766
|
+
const cancelled = /* @__PURE__ */ __name(() => {
|
|
1767
|
+
if (this.generation !== generation) return;
|
|
1768
|
+
this.generation++;
|
|
1769
|
+
this.resolutionFlight = null;
|
|
1770
|
+
this.setUnauthenticated();
|
|
1771
|
+
}, "cancelled");
|
|
1772
|
+
signal?.addEventListener("abort", cancelled, {
|
|
1773
|
+
once: true
|
|
1774
|
+
});
|
|
1775
|
+
if (signal?.aborted) cancelled();
|
|
1776
|
+
return () => signal?.removeEventListener("abort", cancelled);
|
|
1777
|
+
}
|
|
1778
|
+
/** Headers for one sender-constrained resource request. */
|
|
1779
|
+
async authorization(method, url, opts = {}) {
|
|
1780
|
+
const generation = this.generation;
|
|
1781
|
+
const token = await this.getAccessToken(opts);
|
|
1782
|
+
if (!token || this.generation !== generation) return null;
|
|
1783
|
+
if (this.apiKey && token === this.apiKey) return {
|
|
1784
|
+
token,
|
|
1785
|
+
tokenType: "Bearer"
|
|
1786
|
+
};
|
|
1787
|
+
if (!this.access || this.access.token !== token || this.access.tokenType !== "DPoP") return {
|
|
1788
|
+
token,
|
|
1789
|
+
tokenType: "Bearer"
|
|
1790
|
+
};
|
|
1791
|
+
const dpop = await this.createProof({
|
|
1792
|
+
method,
|
|
1793
|
+
url,
|
|
1794
|
+
accessToken: token
|
|
1795
|
+
});
|
|
1796
|
+
if (this.generation !== generation || this.access?.token !== token || this.access.expiresAtMs <= Date.now()) return null;
|
|
1797
|
+
if (!await this.cacheCurrent(this.access) || this.generation !== generation || this.access?.token !== token) return null;
|
|
1798
|
+
return {
|
|
1799
|
+
token,
|
|
1800
|
+
tokenType: "DPoP",
|
|
1801
|
+
headers: {
|
|
1802
|
+
dpop
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
/** Verify and sender-constrain a previously persisted access token. Server
|
|
1807
|
+
* consumers use this for encrypted per-workspace sessions without placing
|
|
1808
|
+
* those tokens into the process-global client cache. */
|
|
1809
|
+
async authorizationForToken(token, method, url, expectedScopes) {
|
|
1810
|
+
await verifyAccessToken({
|
|
1811
|
+
token,
|
|
1812
|
+
transport: this.transport,
|
|
1813
|
+
issuer: this.oidcConfig.issuer,
|
|
1814
|
+
clientId: this.oidcConfig.clientId,
|
|
1815
|
+
dpopJkt: this.oidcConfig.dpopSigner.jkt,
|
|
1816
|
+
jwksUrl: this.oidcConfig.jwksUrl,
|
|
1817
|
+
expectedScopes
|
|
1818
|
+
});
|
|
1819
|
+
const dpop = await this.createProof({
|
|
1820
|
+
method,
|
|
1821
|
+
url,
|
|
1822
|
+
accessToken: token
|
|
1823
|
+
});
|
|
1824
|
+
return {
|
|
1825
|
+
token,
|
|
1826
|
+
tokenType: "DPoP",
|
|
1827
|
+
headers: {
|
|
1828
|
+
dpop
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
/** Verify a provider-originated Back-Channel Logout Token. */
|
|
1833
|
+
verifyBackchannelLogout(token) {
|
|
1834
|
+
return verifyLogoutToken({
|
|
1835
|
+
token,
|
|
1836
|
+
transport: this.transport,
|
|
1837
|
+
issuer: this.oidcConfig.issuer,
|
|
1838
|
+
clientId: this.oidcConfig.clientId,
|
|
1839
|
+
jwksUrl: this.oidcConfig.jwksUrl
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
async createProof(args) {
|
|
1843
|
+
try {
|
|
1844
|
+
return await this.oidcConfig.dpopSigner.createProof(args);
|
|
1845
|
+
} catch {
|
|
1846
|
+
throw new AccountError({
|
|
1847
|
+
code: "UNREACHABLE",
|
|
1848
|
+
message: "Host account request signing is unavailable",
|
|
1849
|
+
retryable: false
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/** Rotate tokens. Persist the returned refreshToken; a failure means re-auth. */
|
|
1854
|
+
refresh(refreshToken) {
|
|
1855
|
+
return refreshTokens(this.oidcConfig, this.transport, refreshToken);
|
|
1856
|
+
}
|
|
1857
|
+
// ─── Device grant (CLI / headless) ──────────────────────────────────────
|
|
1858
|
+
startDeviceLogin(scopes) {
|
|
1859
|
+
return startDeviceAuthorization(this.oidcConfig, this.transport, scopes);
|
|
1860
|
+
}
|
|
1861
|
+
async awaitDeviceLogin(device, opts) {
|
|
1862
|
+
const generation = ++this.generation;
|
|
1863
|
+
this.resolutionFlight = null;
|
|
1864
|
+
const invalidate = this.bindSignInCancellation(generation, opts?.signal);
|
|
1865
|
+
try {
|
|
1866
|
+
if (opts?.signal?.aborted) throw transitionConflict();
|
|
1867
|
+
const transition = await this.reserveSignIn(generation);
|
|
1868
|
+
if (opts?.signal?.aborted || this.generation !== generation) throw transitionConflict();
|
|
1869
|
+
const ts = await pollDeviceToken(this.oidcConfig, this.transport, device, opts);
|
|
1870
|
+
const committed = await this.applyTokenSet(ts, generation, transition, false, opts?.signal);
|
|
1871
|
+
if (this.generation !== generation || opts?.signal?.aborted) {
|
|
1872
|
+
if (committed && opts?.signal?.aborted) await this.cancelCommittedSignIn(committed);
|
|
1873
|
+
throw transitionConflict();
|
|
1874
|
+
}
|
|
1875
|
+
return ts;
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
if (opts?.signal?.aborted) throw transitionConflict();
|
|
1878
|
+
throw error;
|
|
1879
|
+
} finally {
|
|
1880
|
+
invalidate();
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
function accountFromVerifiedClaims(p) {
|
|
1885
|
+
if (!p.sub) return null;
|
|
1886
|
+
return {
|
|
1887
|
+
id: p.sub,
|
|
1888
|
+
email: p.email ?? "",
|
|
1889
|
+
username: p.preferred_username ?? null,
|
|
1890
|
+
displayName: p.name ?? null,
|
|
1891
|
+
avatarUrl: p.picture ?? null,
|
|
1892
|
+
emailVerified: p.email_verified === true,
|
|
1893
|
+
linkedSurfaces: []
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
__name(accountFromVerifiedClaims, "accountFromVerifiedClaims");
|
|
1897
|
+
function containsScopes(held, requested) {
|
|
1898
|
+
if (!requested?.length) return true;
|
|
1899
|
+
const set = new Set(held);
|
|
1900
|
+
return requested.every((scope) => set.has(scope));
|
|
1901
|
+
}
|
|
1902
|
+
__name(containsScopes, "containsScopes");
|
|
1903
|
+
function transitionConflict() {
|
|
1904
|
+
return new AccountError({
|
|
1905
|
+
code: "CONFLICT",
|
|
1906
|
+
message: "Account credential transition changed; explicit recovery required",
|
|
1907
|
+
retryable: false
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
__name(transitionConflict, "transitionConflict");
|
|
1911
|
+
|
|
1912
|
+
// src/dpop.ts
|
|
1913
|
+
function normalizeDpopHtu(value) {
|
|
1914
|
+
const url = new URL(value);
|
|
1915
|
+
if (url.protocol !== "https:") throw new Error("DPoP htu must use HTTPS");
|
|
1916
|
+
url.hash = "";
|
|
1917
|
+
url.search = "";
|
|
1918
|
+
url.username = "";
|
|
1919
|
+
url.password = "";
|
|
1920
|
+
url.hostname = url.hostname.toLowerCase();
|
|
1921
|
+
if (url.port === "443") url.port = "";
|
|
1922
|
+
return url.toString();
|
|
1923
|
+
}
|
|
1924
|
+
__name(normalizeDpopHtu, "normalizeDpopHtu");
|
|
1925
|
+
|
|
1926
|
+
// src/bff.ts
|
|
1927
|
+
var BffSessionClient = class {
|
|
1928
|
+
static {
|
|
1929
|
+
__name(this, "BffSessionClient");
|
|
1930
|
+
}
|
|
1931
|
+
baseUrl;
|
|
1932
|
+
fetchImpl;
|
|
1933
|
+
csrf;
|
|
1934
|
+
constructor(baseUrl = "/api/session", fetchImpl = globalThis.fetch, csrf) {
|
|
1935
|
+
this.baseUrl = baseUrl;
|
|
1936
|
+
this.fetchImpl = fetchImpl;
|
|
1937
|
+
this.csrf = csrf;
|
|
1938
|
+
}
|
|
1939
|
+
loginUrl(returnUrl) {
|
|
1940
|
+
if (!returnUrl.startsWith("/") || returnUrl.startsWith("//")) throw new AccountError({
|
|
1941
|
+
code: "BAD_REQUEST",
|
|
1942
|
+
message: "returnUrl must be same-origin"
|
|
1943
|
+
});
|
|
1944
|
+
return `${this.baseUrl}/login/start?return_url=${encodeURIComponent(returnUrl)}`;
|
|
1945
|
+
}
|
|
1946
|
+
session() {
|
|
1947
|
+
return this.request("GET", "/session");
|
|
1948
|
+
}
|
|
1949
|
+
refresh(idempotencyKey) {
|
|
1950
|
+
return this.request("POST", "/refresh", idempotencyKey);
|
|
1951
|
+
}
|
|
1952
|
+
async logout() {
|
|
1953
|
+
await this.request("POST", "/logout");
|
|
1954
|
+
}
|
|
1955
|
+
async request(method, path, idempotencyKey) {
|
|
1956
|
+
const headers = {
|
|
1957
|
+
accept: "application/json"
|
|
1958
|
+
};
|
|
1959
|
+
if (method === "POST") {
|
|
1960
|
+
const csrf = this.csrf();
|
|
1961
|
+
if (!csrf) throw new AccountError({
|
|
1962
|
+
code: "BAD_REQUEST",
|
|
1963
|
+
message: "missing BFF CSRF value"
|
|
1964
|
+
});
|
|
1965
|
+
headers["x-xeno-csrf"] = csrf;
|
|
1966
|
+
if (idempotencyKey) headers["idempotency-key"] = idempotencyKey;
|
|
1967
|
+
}
|
|
1968
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
1969
|
+
method,
|
|
1970
|
+
credentials: "include",
|
|
1971
|
+
headers
|
|
1972
|
+
});
|
|
1973
|
+
if (!response.ok) throw new AccountError({
|
|
1974
|
+
code: response.status === 401 ? "INVALID_TOKEN" : "PLATFORM_ERROR",
|
|
1975
|
+
message: `BFF session request failed: ${response.status}`,
|
|
1976
|
+
httpStatus: response.status
|
|
1977
|
+
});
|
|
1978
|
+
if (response.status === 204) return void 0;
|
|
1979
|
+
return response.json();
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
// src/redaction.ts
|
|
1984
|
+
var TOKEN_RE = /(eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,})|([A-Za-z0-9_-]{40,})/g;
|
|
1985
|
+
function redact(input) {
|
|
1986
|
+
const s = typeof input === "string" ? input : safeStringify(input);
|
|
1987
|
+
return s.replace(TOKEN_RE, (m) => `${m.slice(0, 6)}\u2026[redacted:${m.length}]`);
|
|
1988
|
+
}
|
|
1989
|
+
__name(redact, "redact");
|
|
1990
|
+
function safeStringify(v) {
|
|
1991
|
+
try {
|
|
1992
|
+
return JSON.stringify(v) ?? String(v);
|
|
1993
|
+
} catch {
|
|
1994
|
+
return String(v);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
__name(safeStringify, "safeStringify");
|
|
1998
|
+
|
|
1999
|
+
// src/resource-authorization.ts
|
|
2000
|
+
var MAX_JWT_BYTES = 16384;
|
|
2001
|
+
var PRIVATE_FIELDS = [
|
|
2002
|
+
"d",
|
|
2003
|
+
"p",
|
|
2004
|
+
"q",
|
|
2005
|
+
"dp",
|
|
2006
|
+
"dq",
|
|
2007
|
+
"qi",
|
|
2008
|
+
"oth",
|
|
2009
|
+
"k"
|
|
2010
|
+
];
|
|
2011
|
+
function denied() {
|
|
2012
|
+
return new AccountError({
|
|
2013
|
+
code: "INVALID_TOKEN",
|
|
2014
|
+
message: "Resource authorization refused",
|
|
2015
|
+
retryable: false,
|
|
2016
|
+
httpStatus: 401
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
__name(denied, "denied");
|
|
2020
|
+
function unavailable() {
|
|
2021
|
+
return new AccountError({
|
|
2022
|
+
code: "UNREACHABLE",
|
|
2023
|
+
message: "Resource authorization authority unavailable",
|
|
2024
|
+
retryable: true,
|
|
2025
|
+
httpStatus: 503
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
__name(unavailable, "unavailable");
|
|
2029
|
+
function decodePart(part) {
|
|
2030
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(part) || part.length % 4 === 1) throw denied();
|
|
2031
|
+
const padded = part.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - part.length % 4) % 4);
|
|
2032
|
+
try {
|
|
2033
|
+
return Uint8Array.from(atob(padded), (value) => value.charCodeAt(0));
|
|
2034
|
+
} catch {
|
|
2035
|
+
throw denied();
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
__name(decodePart, "decodePart");
|
|
2039
|
+
function record(bytes2) {
|
|
2040
|
+
try {
|
|
2041
|
+
return parseJwtObject(new TextDecoder("utf-8", {
|
|
2042
|
+
fatal: true
|
|
2043
|
+
}).decode(bytes2));
|
|
2044
|
+
} catch {
|
|
2045
|
+
throw denied();
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
__name(record, "record");
|
|
2049
|
+
async function sha(value) {
|
|
2050
|
+
const bytes2 = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)));
|
|
2051
|
+
return btoa(String.fromCharCode(...bytes2)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
2052
|
+
}
|
|
2053
|
+
__name(sha, "sha");
|
|
2054
|
+
function frozenClaims(claims) {
|
|
2055
|
+
return Object.freeze({
|
|
2056
|
+
iss: claims.iss,
|
|
2057
|
+
sub: claims.sub,
|
|
2058
|
+
aud: claims.aud,
|
|
2059
|
+
exp: claims.exp,
|
|
2060
|
+
iat: claims.iat,
|
|
2061
|
+
sid: claims.sid,
|
|
2062
|
+
client_id: claims.client_id,
|
|
2063
|
+
azp: claims.azp,
|
|
2064
|
+
scope: claims.scope,
|
|
2065
|
+
typ: claims.typ,
|
|
2066
|
+
auth_epoch: claims.auth_epoch,
|
|
2067
|
+
...claims.auth_time === void 0 ? {} : {
|
|
2068
|
+
auth_time: claims.auth_time
|
|
2069
|
+
},
|
|
2070
|
+
...claims.acr ? {
|
|
2071
|
+
acr: claims.acr
|
|
2072
|
+
} : {},
|
|
2073
|
+
cnf: Object.freeze({
|
|
2074
|
+
jkt: claims.cnf.jkt
|
|
2075
|
+
}),
|
|
2076
|
+
...Array.isArray(claims.aud) ? {
|
|
2077
|
+
aud: Object.freeze([
|
|
2078
|
+
...claims.aud
|
|
2079
|
+
])
|
|
2080
|
+
} : {},
|
|
2081
|
+
...claims.act ? {
|
|
2082
|
+
act: Object.freeze({
|
|
2083
|
+
client_id: claims.act.client_id,
|
|
2084
|
+
installation_id_hash: claims.act.installation_id_hash,
|
|
2085
|
+
...claims.act.sub ? {
|
|
2086
|
+
sub: claims.act.sub
|
|
2087
|
+
} : {}
|
|
2088
|
+
})
|
|
2089
|
+
} : {},
|
|
2090
|
+
...claims.amr ? {
|
|
2091
|
+
amr: Object.freeze([
|
|
2092
|
+
...claims.amr
|
|
2093
|
+
])
|
|
2094
|
+
} : {}
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
__name(frozenClaims, "frozenClaims");
|
|
2098
|
+
function createResourceRequestAuthorizer(options) {
|
|
2099
|
+
const issuer = normalizeDpopHtu(options.issuer);
|
|
2100
|
+
if (issuer.replace(/\/$/, "") !== options.issuer || !options.clientIds.length || options.clientIds.length > 128 || options.clientIds.some((id) => !/^[a-zA-Z0-9._-]{1,128}$/.test(id)) || new Set(options.clientIds).size !== options.clientIds.length) throw new TypeError("Explicit canonical issuer and unique clients required");
|
|
2101
|
+
const clients = new Set(options.clientIds);
|
|
2102
|
+
return {
|
|
2103
|
+
async authorize(request) {
|
|
2104
|
+
request = Object.freeze({
|
|
2105
|
+
...request,
|
|
2106
|
+
...request.scopes ? {
|
|
2107
|
+
scopes: [
|
|
2108
|
+
...request.scopes
|
|
2109
|
+
]
|
|
2110
|
+
} : {}
|
|
2111
|
+
});
|
|
2112
|
+
if (!/^[A-Z]{3,16}$/.test(request.method) || request.url.length > 8192 || request.authorization.length > MAX_JWT_BYTES + 5 || request.proof.length > MAX_JWT_BYTES || !request.proof || (request.scopes?.length ?? 0) > 128) throw denied();
|
|
2113
|
+
let url;
|
|
2114
|
+
try {
|
|
2115
|
+
const parsed = new URL(request.url);
|
|
2116
|
+
if (parsed.username || parsed.password || parsed.hash) throw denied();
|
|
2117
|
+
url = normalizeDpopHtu(request.url);
|
|
2118
|
+
} catch {
|
|
2119
|
+
throw denied();
|
|
2120
|
+
}
|
|
2121
|
+
const match = /^DPoP ([a-zA-Z0-9_.-]+)$/.exec(request.authorization);
|
|
2122
|
+
if (!match) throw denied();
|
|
2123
|
+
const token = match[1];
|
|
2124
|
+
const parts = request.proof.split(".");
|
|
2125
|
+
if (parts.length !== 3) throw denied();
|
|
2126
|
+
const header = record(decodePart(parts[0])), proof = record(decodePart(parts[1]));
|
|
2127
|
+
if (header.typ !== "dpop+jwt" || header.alg !== "ES256" || header.crit !== void 0 || header.b64 !== void 0 || !header.jwk || typeof header.jwk !== "object" || Array.isArray(header.jwk)) throw denied();
|
|
2128
|
+
const supplied = header.jwk;
|
|
2129
|
+
if (PRIVATE_FIELDS.some((field) => supplied[field] !== void 0) || supplied.kty !== "EC" || supplied.crv !== "P-256" || typeof supplied.x !== "string" || typeof supplied.y !== "string" || decodePart(supplied.x).length !== 32 || decodePart(supplied.y).length !== 32) throw denied();
|
|
2130
|
+
const jwk = {
|
|
2131
|
+
kty: "EC",
|
|
2132
|
+
crv: "P-256",
|
|
2133
|
+
x: supplied.x,
|
|
2134
|
+
y: supplied.y
|
|
2135
|
+
};
|
|
2136
|
+
const jkt = await sha(JSON.stringify({
|
|
2137
|
+
crv: jwk.crv,
|
|
2138
|
+
kty: jwk.kty,
|
|
2139
|
+
x: jwk.x,
|
|
2140
|
+
y: jwk.y
|
|
2141
|
+
}));
|
|
2142
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2143
|
+
if (typeof proof.jti !== "string" || !/^[a-zA-Z0-9._:-]{1,128}$/.test(proof.jti) || !Number.isSafeInteger(proof.iat) || Number(proof.iat) < now - 60 || Number(proof.iat) > now + 60 || proof.htm !== request.method || proof.htu !== url || proof.ath !== await sha(token) || (request.nonce === void 0 ? proof.nonce !== void 0 : proof.nonce !== request.nonce)) throw denied();
|
|
2144
|
+
try {
|
|
2145
|
+
const key = await crypto.subtle.importKey("jwk", jwk, {
|
|
2146
|
+
name: "ECDSA",
|
|
2147
|
+
namedCurve: "P-256"
|
|
2148
|
+
}, false, [
|
|
2149
|
+
"verify"
|
|
2150
|
+
]);
|
|
2151
|
+
if (!await crypto.subtle.verify({
|
|
2152
|
+
name: "ECDSA",
|
|
2153
|
+
hash: "SHA-256"
|
|
2154
|
+
}, key, decodePart(parts[2]), new TextEncoder().encode(`${parts[0]}.${parts[1]}`))) throw denied();
|
|
2155
|
+
} catch {
|
|
2156
|
+
throw denied();
|
|
2157
|
+
}
|
|
2158
|
+
const tokenParts = token.split(".");
|
|
2159
|
+
if (tokenParts.length !== 3) throw denied();
|
|
2160
|
+
const tokenHeader = record(decodePart(tokenParts[0]));
|
|
2161
|
+
if (tokenHeader.alg !== "ES256" || tokenHeader.typ !== "at+jwt" || typeof tokenHeader.kid !== "string" || tokenHeader.kid.length > 128 || tokenHeader.crit !== void 0 || tokenHeader.b64 !== void 0 || tokenHeader.jku !== void 0 || tokenHeader.jwk !== void 0 || tokenHeader.x5u !== void 0) throw denied();
|
|
2162
|
+
const untrusted = record(decodePart(tokenParts[1]));
|
|
2163
|
+
if (untrusted.auth_time !== void 0 && (!Number.isSafeInteger(untrusted.auth_time) || Number(untrusted.auth_time) < 0 || Number(untrusted.auth_time) > now + 30)) throw denied();
|
|
2164
|
+
if (untrusted.acr !== void 0 && (typeof untrusted.acr !== "string" || untrusted.acr.length > 128)) throw denied();
|
|
2165
|
+
if (untrusted.amr !== void 0 && (!Array.isArray(untrusted.amr) || untrusted.amr.length > 16 || untrusted.amr.some((method) => typeof method !== "string" || !/^[a-zA-Z0-9._:-]{1,64}$/.test(method)))) throw denied();
|
|
2166
|
+
if (typeof untrusted.client_id !== "string" || !clients.has(untrusted.client_id)) throw denied();
|
|
2167
|
+
let verified;
|
|
2168
|
+
try {
|
|
2169
|
+
verified = await verifyAccessToken({
|
|
2170
|
+
token,
|
|
2171
|
+
transport: options.transport,
|
|
2172
|
+
issuer: options.issuer,
|
|
2173
|
+
clientId: untrusted.client_id,
|
|
2174
|
+
dpopJkt: jkt,
|
|
2175
|
+
expectedScopes: [
|
|
2176
|
+
...request.scopes ?? []
|
|
2177
|
+
]
|
|
2178
|
+
});
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
if (error instanceof AccountError && error.retryable) throw unavailable();
|
|
2181
|
+
throw denied();
|
|
2182
|
+
}
|
|
2183
|
+
if (!verified.sub || verified.sub.length > 256 || !verified.sid || verified.sid.length > 256 || !Number.isSafeInteger(verified.auth_epoch) || verified.auth_epoch < 0 || typeof verified.scope !== "string" || verified.scope.length > 8192) throw denied();
|
|
2184
|
+
const claims = frozenClaims(verified);
|
|
2185
|
+
const active = /* @__PURE__ */ __name(async () => {
|
|
2186
|
+
if (claims.exp <= Math.floor(Date.now() / 1e3)) throw denied();
|
|
2187
|
+
let current;
|
|
2188
|
+
try {
|
|
2189
|
+
current = await options.sessionActive(claims, token);
|
|
2190
|
+
} catch {
|
|
2191
|
+
throw unavailable();
|
|
2192
|
+
}
|
|
2193
|
+
if (current !== true) throw denied();
|
|
2194
|
+
}, "active");
|
|
2195
|
+
await active();
|
|
2196
|
+
let consumed;
|
|
2197
|
+
try {
|
|
2198
|
+
consumed = await options.replayStore.consume({
|
|
2199
|
+
keyThumbprint: jkt,
|
|
2200
|
+
proofId: proof.jti,
|
|
2201
|
+
method: request.method,
|
|
2202
|
+
url,
|
|
2203
|
+
expiresAtMs: Math.min(claims.exp * 1e3, (Number(proof.iat) + 61) * 1e3)
|
|
2204
|
+
});
|
|
2205
|
+
} catch {
|
|
2206
|
+
throw unavailable();
|
|
2207
|
+
}
|
|
2208
|
+
if (consumed !== true) throw denied();
|
|
2209
|
+
await active();
|
|
2210
|
+
return Object.freeze({
|
|
2211
|
+
claims,
|
|
2212
|
+
revalidate: active
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
__name(createResourceRequestAuthorizer, "createResourceRequestAuthorizer");
|
|
2218
|
+
function createOidcIntrospectionSessionAuthority(options) {
|
|
2219
|
+
const base = new URL(options.issuer);
|
|
2220
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
2221
|
+
if (base.origin !== options.issuer || base.protocol !== "https:" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e4) throw new TypeError("Canonical HTTPS issuer and bounded introspection timeout required");
|
|
2222
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2223
|
+
return async (claims, token) => {
|
|
2224
|
+
if (token.length > MAX_JWT_BYTES || !token) throw unavailable();
|
|
2225
|
+
const controller = new AbortController();
|
|
2226
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2227
|
+
let response;
|
|
2228
|
+
try {
|
|
2229
|
+
response = await fetchBeforeAbort(fetchImpl, `${base.origin}/api/oauth2/introspect`, {
|
|
2230
|
+
method: "POST",
|
|
2231
|
+
redirect: "error",
|
|
2232
|
+
cache: "no-store",
|
|
2233
|
+
credentials: "omit",
|
|
2234
|
+
signal: controller.signal,
|
|
2235
|
+
headers: {
|
|
2236
|
+
accept: "application/json",
|
|
2237
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
2238
|
+
},
|
|
2239
|
+
body: new URLSearchParams({
|
|
2240
|
+
token
|
|
2241
|
+
}).toString()
|
|
2242
|
+
}, controller.signal);
|
|
2243
|
+
if (!response.ok || !response.body) throw unavailable();
|
|
2244
|
+
const value = parseJwtObject(await readBoundedText(response, controller.signal, 32768));
|
|
2245
|
+
if (value.active === false) return false;
|
|
2246
|
+
const canonicalAudience = Array.isArray(value.aud) ? value.aud.includes("xeno-api") : value.aud === "xeno-api";
|
|
2247
|
+
return value.active === true && value.token_type === "access_token" && value.iss === claims.iss && value.sub === claims.sub && value.sid === claims.sid && value.client_id === claims.client_id && value.exp === claims.exp && value.scope === claims.scope && canonicalAudience;
|
|
2248
|
+
} catch {
|
|
2249
|
+
throw unavailable();
|
|
2250
|
+
} finally {
|
|
2251
|
+
controller.abort();
|
|
2252
|
+
clearTimeout(timer);
|
|
2253
|
+
if (response) cancelBody(response);
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
__name(createOidcIntrospectionSessionAuthority, "createOidcIntrospectionSessionAuthority");
|
|
2258
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2259
|
+
0 && (module.exports = {
|
|
2260
|
+
AccountClient,
|
|
2261
|
+
AccountError,
|
|
2262
|
+
AuthzClient,
|
|
2263
|
+
BffSessionClient,
|
|
2264
|
+
LedgerClient,
|
|
2265
|
+
MICRO_PER_CREDIT,
|
|
2266
|
+
Transport,
|
|
2267
|
+
XenoAccountClient,
|
|
2268
|
+
accountSchema,
|
|
2269
|
+
authzCheckSchema,
|
|
2270
|
+
authzResultSchema,
|
|
2271
|
+
balanceSchema,
|
|
2272
|
+
buildAuthorizeUrl,
|
|
2273
|
+
codeForStatus,
|
|
2274
|
+
createOidcIntrospectionSessionAuthority,
|
|
2275
|
+
createResourceRequestAuthorizer,
|
|
2276
|
+
exchangeCode,
|
|
2277
|
+
fromMicro,
|
|
2278
|
+
generatePkce,
|
|
2279
|
+
generateState,
|
|
2280
|
+
holdRequestSchema,
|
|
2281
|
+
holdSchema,
|
|
2282
|
+
keychainStore,
|
|
2283
|
+
memoryStore,
|
|
2284
|
+
meterAggregationSchema,
|
|
2285
|
+
normalize,
|
|
2286
|
+
normalizeDpopHtu,
|
|
2287
|
+
oauthTokenResponseSchema,
|
|
2288
|
+
pollDeviceToken,
|
|
2289
|
+
redact,
|
|
2290
|
+
refreshTokens,
|
|
2291
|
+
startDeviceAuthorization,
|
|
2292
|
+
surfaceSchema,
|
|
2293
|
+
toMicro,
|
|
2294
|
+
tokenSetSchema,
|
|
2295
|
+
usageEventSchema,
|
|
2296
|
+
usageResultSchema,
|
|
2297
|
+
verifyAccessToken,
|
|
2298
|
+
verifyIdToken,
|
|
2299
|
+
verifyLogoutToken
|
|
2300
|
+
});
|
|
2301
|
+
//# sourceMappingURL=index.js.map
|