@route_33/opencode 0.4.6 → 0.4.7-rc.2
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/README.md +71 -95
- package/dist/cli/production-smoke/production-smoke.js +1649 -0
- package/dist/index.js +3577 -1266
- package/dist/tui-entry/index.js +6987 -0
- package/dist/v2-entry/index.js +4203 -0
- package/package.json +38 -42
- package/LICENSE +0 -21
- package/dist/index.d.ts +0 -23
- package/dist/tui/index.d.ts +0 -5
- package/dist/tui/index.js +0 -542
- package/docs/gateway-limits-variants-broadcast.md +0 -146
- package/docs/gateway-pricing-proposal.md +0 -96
- package/docs/getting-started.html +0 -570
|
@@ -0,0 +1,1649 @@
|
|
|
1
|
+
// src/cli/production-smoke/production-smoke.ts
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
// src/requested-client-id/requested-client-id.ts
|
|
5
|
+
var DEFAULT_ROUTE33_CLIENT_ID = false ? "" : "oa_platform_203623de";
|
|
6
|
+
|
|
7
|
+
// src/shared/logger/logger.ts
|
|
8
|
+
var logger = {
|
|
9
|
+
info: (...args) => {
|
|
10
|
+
console.log("[route_33]", ...args);
|
|
11
|
+
},
|
|
12
|
+
warn: (...args) => {
|
|
13
|
+
console.warn("[route_33]", ...args);
|
|
14
|
+
},
|
|
15
|
+
error: (...args) => {
|
|
16
|
+
console.error("[route_33]", ...args);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/auth/auth-error/auth-error.ts
|
|
21
|
+
var AuthError = class extends Error {
|
|
22
|
+
constructor(code, options) {
|
|
23
|
+
super(
|
|
24
|
+
"Route 33 authorization failed",
|
|
25
|
+
options?.cause === void 0 ? void 0 : { cause: options.cause }
|
|
26
|
+
);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.name = "AuthError";
|
|
29
|
+
this.statusCode = options?.statusCode;
|
|
30
|
+
}
|
|
31
|
+
code;
|
|
32
|
+
statusCode;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/auth/build-authorize-url/build-authorize-url.ts
|
|
36
|
+
var buildAuthorizeUrl = (issuerUrl, request) => {
|
|
37
|
+
const url = new URL("/authorize", issuerUrl);
|
|
38
|
+
url.search = new URLSearchParams({
|
|
39
|
+
client_id: request.clientId,
|
|
40
|
+
code_challenge: request.codeChallenge,
|
|
41
|
+
code_challenge_method: "S256",
|
|
42
|
+
redirect_uri: request.redirectUri,
|
|
43
|
+
response_type: "code",
|
|
44
|
+
scope: request.scope,
|
|
45
|
+
state: request.state
|
|
46
|
+
}).toString();
|
|
47
|
+
return url;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/contract/adapters/contract-parse-error/contract-parse-error.ts
|
|
51
|
+
var ContractParseError = class extends Error {
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/contract/adapters/gateway-api-key/gateway-api-key.ts
|
|
55
|
+
var GatewayApiKey = class {
|
|
56
|
+
value;
|
|
57
|
+
constructor(value) {
|
|
58
|
+
this.value = value;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/contract/adapters/management-jwt/management-jwt.ts
|
|
63
|
+
var ManagementJwt = class {
|
|
64
|
+
value;
|
|
65
|
+
constructor(value) {
|
|
66
|
+
this.value = value;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/contract/adapters/parse-management-jwt/parse-management-jwt.ts
|
|
71
|
+
var parseManagementJwt = (value) => {
|
|
72
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
73
|
+
throw new Error("management token must be non-empty");
|
|
74
|
+
}
|
|
75
|
+
return new ManagementJwt(value);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/contract/predicates/has-string-fields/has-string-fields.ts
|
|
79
|
+
var hasStringFields = (record, fields) => fields.every((field) => typeof record[field] === "string");
|
|
80
|
+
|
|
81
|
+
// src/contract/predicates/is-nullable-string/is-nullable-string.ts
|
|
82
|
+
var isNullableString = (value) => typeof value === "string" || value === null;
|
|
83
|
+
|
|
84
|
+
// src/contract/predicates/is-owner-type/is-owner-type.ts
|
|
85
|
+
var isOwnerType = (value) => value === "project" || value === "user_personal" || value === "user_project";
|
|
86
|
+
|
|
87
|
+
// src/contract/predicates/is-record/is-record.ts
|
|
88
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
|
|
90
|
+
// src/contract/adapters/parse-management-subject/parse-management-subject.ts
|
|
91
|
+
var parseManagementSubject = (credential) => {
|
|
92
|
+
const encodedPayload = credential.value.split(".")[1];
|
|
93
|
+
if (encodedPayload === void 0 || encodedPayload.length === 0) {
|
|
94
|
+
throw new ContractParseError("management token carries no usable subject");
|
|
95
|
+
}
|
|
96
|
+
let payload;
|
|
97
|
+
try {
|
|
98
|
+
payload = JSON.parse(
|
|
99
|
+
Buffer.from(encodedPayload, "base64url").toString("utf8")
|
|
100
|
+
);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new ContractParseError("management token payload is not JSON");
|
|
103
|
+
}
|
|
104
|
+
if (!isRecord(payload)) {
|
|
105
|
+
throw new ContractParseError("management token payload is not an object");
|
|
106
|
+
}
|
|
107
|
+
const subject = payload["sub"];
|
|
108
|
+
if (typeof subject !== "string" || subject.length === 0) {
|
|
109
|
+
throw new ContractParseError("management token carries no usable subject");
|
|
110
|
+
}
|
|
111
|
+
return subject;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/contract/auth-session/parse-token-set/parse-token-set.ts
|
|
115
|
+
var parseTokenSet = (value) => {
|
|
116
|
+
if (!isRecord(value)) {
|
|
117
|
+
throw new ContractParseError("invalid token response");
|
|
118
|
+
}
|
|
119
|
+
if (typeof value["access_token"] !== "string" || typeof value["expires_in"] !== "number" || value["token_type"] !== "Bearer") {
|
|
120
|
+
throw new ContractParseError("invalid token response");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
access_token: value["access_token"],
|
|
124
|
+
expires_in: value["expires_in"],
|
|
125
|
+
token_type: "Bearer",
|
|
126
|
+
...typeof value["id_token"] === "string" ? { id_token: value["id_token"] } : {},
|
|
127
|
+
...typeof value["refresh_token"] === "string" ? { refresh_token: value["refresh_token"] } : {},
|
|
128
|
+
...typeof value["scope"] === "string" ? { scope: value["scope"] } : {}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/auth/exchange-code-and-parse/exchange-code-and-parse.ts
|
|
133
|
+
var exchangeCodeAndParse = async (options) => {
|
|
134
|
+
const body = new URLSearchParams({
|
|
135
|
+
client_id: options.clientId,
|
|
136
|
+
code: options.input.code,
|
|
137
|
+
code_verifier: options.input.codeVerifier,
|
|
138
|
+
grant_type: "authorization_code",
|
|
139
|
+
redirect_uri: options.input.redirectUri
|
|
140
|
+
});
|
|
141
|
+
let response;
|
|
142
|
+
try {
|
|
143
|
+
response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
|
|
144
|
+
body,
|
|
145
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
146
|
+
method: "POST"
|
|
147
|
+
});
|
|
148
|
+
} catch (cause) {
|
|
149
|
+
throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
|
|
150
|
+
}
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", {
|
|
153
|
+
statusCode: response.status
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
let payload;
|
|
157
|
+
try {
|
|
158
|
+
payload = await response.json();
|
|
159
|
+
} catch (cause) {
|
|
160
|
+
throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
return parseTokenSet(payload);
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
if (cause instanceof ContractParseError) {
|
|
166
|
+
throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
|
|
167
|
+
}
|
|
168
|
+
throw cause;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/auth/refresh-session/refresh-session.ts
|
|
173
|
+
var refreshSession = async (options) => {
|
|
174
|
+
const body = new URLSearchParams({
|
|
175
|
+
client_id: options.clientId,
|
|
176
|
+
grant_type: "refresh_token",
|
|
177
|
+
refresh_token: options.refreshToken
|
|
178
|
+
});
|
|
179
|
+
let response;
|
|
180
|
+
try {
|
|
181
|
+
response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
|
|
182
|
+
body,
|
|
183
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
184
|
+
method: "POST"
|
|
185
|
+
});
|
|
186
|
+
} catch (cause) {
|
|
187
|
+
throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
|
|
188
|
+
}
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", {
|
|
191
|
+
statusCode: response.status
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
let payload;
|
|
195
|
+
try {
|
|
196
|
+
payload = await response.json();
|
|
197
|
+
} catch (cause) {
|
|
198
|
+
throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return parseTokenSet(payload);
|
|
202
|
+
} catch (cause) {
|
|
203
|
+
if (cause instanceof ContractParseError) {
|
|
204
|
+
throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
|
|
205
|
+
}
|
|
206
|
+
throw cause;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/auth/create-auth-session/create-auth-session.ts
|
|
211
|
+
var createAuthSession = (options) => {
|
|
212
|
+
const tokenUrl = new URL("/oauth/token", options.issuerUrl);
|
|
213
|
+
return {
|
|
214
|
+
authorizationUrl: (request) => buildAuthorizeUrl(options.issuerUrl, request),
|
|
215
|
+
exchangeCode: (input) => exchangeCodeAndParse({
|
|
216
|
+
clientId: options.clientId,
|
|
217
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
|
|
218
|
+
input,
|
|
219
|
+
tokenUrl
|
|
220
|
+
}),
|
|
221
|
+
refresh: (refreshToken) => refreshSession({
|
|
222
|
+
clientId: options.clientId,
|
|
223
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
|
|
224
|
+
refreshToken,
|
|
225
|
+
tokenUrl
|
|
226
|
+
})
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/auth/open-browser-and-capture/open-browser-and-capture.ts
|
|
231
|
+
import { spawn } from "node:child_process";
|
|
232
|
+
import { timingSafeEqual } from "node:crypto";
|
|
233
|
+
|
|
234
|
+
// src/auth/loopback-callback/loopback-callback.ts
|
|
235
|
+
import { createServer } from "node:http";
|
|
236
|
+
|
|
237
|
+
// src/auth/loopback-callback/callback-page/callback-page.ts
|
|
238
|
+
var MARK_PATHS = [
|
|
239
|
+
"M310.6,175.8c0-6.1-2.7-11.8-7.2-15.9l-127.6-114.4V7h-43.5v48.3c0,6.2,2.6,12.1,7.2,16.2l92.5,82.9H0v43.5h234.7l-95.6,91.4c-4.3,4.1-6.7,9.8-6.7,15.7v48.3h43.5v-39l128.1-122.4c4.4-4.2,6.8-10,6.7-16.1h0Z",
|
|
240
|
+
"M457.2,175.9v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4-13.3,14.8-23.2,19.5-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,9,19.4,9,31.2s-3,21.2-9.1,29.8-14.4,15.3-25.1,20.1c-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2c18.6-8.8,33.3-20.8,44-36,10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6c-12.8-14.9-30.8-24.2-54-27.8h0Z",
|
|
241
|
+
"M763,203.7c-12.8-14.9-30.8-24.2-54-27.8v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-8.9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4c-5.6,8.3-13.3,14.8-23.2,19.5s-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,8.9,19.4,8.9,31.2s-3,21.2-9.1,29.8c-6.1,8.6-14.4,15.3-25.1,20.1-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2,33.3-20.8,44-36c10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6h0Z"
|
|
242
|
+
];
|
|
243
|
+
var BRAND_MARK = `<svg class="r33-mark" viewBox="0 0 782.2 360.3" fill="currentColor" role="img" aria-label="route33.ai" xmlns="http://www.w3.org/2000/svg"><path d="${MARK_PATHS[0]}"/><path d="${MARK_PATHS[1]}"/><path d="${MARK_PATHS[2]}"/></svg>`;
|
|
244
|
+
var PAGE_STYLE = `:root{color-scheme:dark;--r33-euro-blue:#0000FF;--r33-off-white:#F1F1F1;--r33-off-black:#121212;--r33-grey-300:#C9CBCF;--r33-radius-fillet:4px;--r33-space-2:8px;--r33-space-3:16px;--r33-space-4:24px;--r33-space-6:64px;--r33-font-body:"Geist","Helvetica Neue",Arial,sans-serif;--r33-font-mono:"Geist Mono","SFMono-Regular",Menlo,monospace;--r33-surface:var(--r33-off-black);--r33-text-primary:var(--r33-off-white);--r33-text-secondary:var(--r33-grey-300);--r33-fill-accent:var(--r33-euro-blue);--r33-on-fill-accent:var(--r33-off-white)}
|
|
245
|
+
*{box-sizing:border-box}
|
|
246
|
+
html,body{margin:0}
|
|
247
|
+
.r33-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:var(--r33-space-6);background:var(--r33-surface);color:var(--r33-text-primary);font-family:var(--r33-font-body);-webkit-font-smoothing:antialiased}
|
|
248
|
+
.r33-panel{display:grid;gap:var(--r33-space-4);max-width:460px;width:100%}
|
|
249
|
+
.r33-mark{height:40px;width:auto;color:var(--r33-text-primary)}
|
|
250
|
+
.r33-status{display:inline-flex;align-items:center;gap:var(--r33-space-2);font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--r33-text-secondary)}
|
|
251
|
+
.r33-status__dot{width:8px;height:8px;background:var(--r33-fill-accent)}
|
|
252
|
+
.r33-status__dot--error{background:transparent;border:1px solid var(--r33-text-primary);border-radius:50%}
|
|
253
|
+
.r33-headline{margin:0;font-size:48px;line-height:0.85;letter-spacing:-0.02em;font-weight:600;color:var(--r33-text-primary);text-wrap:balance}
|
|
254
|
+
.r33-subline{margin:0;font-size:16px;line-height:1.45;color:var(--r33-text-secondary);max-width:40ch;text-wrap:pretty}
|
|
255
|
+
.r33-code{margin:0;font-size:13px;letter-spacing:0.04em;color:var(--r33-text-secondary)}
|
|
256
|
+
.r33-code__value{font-family:var(--r33-font-mono);text-transform:uppercase;color:var(--r33-text-primary)}
|
|
257
|
+
@media (prefers-reduced-motion:reduce){*{animation-duration:1ms!important;transition-duration:1ms!important}}`;
|
|
258
|
+
var buildCallbackPage = (state, errorCode) => {
|
|
259
|
+
const isError = state === "error";
|
|
260
|
+
const safeCode = (errorCode ?? "").replace(/[^a-z0-9_-]/gi, "");
|
|
261
|
+
const title = isError ? "Sign-in failed" : "Signed in";
|
|
262
|
+
const headline = isError ? "Sign-in failed" : "Signed in to route33";
|
|
263
|
+
const subline = isError ? "Return to OpenCode and retry." : "Return to OpenCode \u2014 one more step may wait there: if it asks who pays for usage, pick a project. Then this window can be closed.";
|
|
264
|
+
const statusLabel = isError ? "Authentication error" : "Authenticated";
|
|
265
|
+
const dotClass = isError ? "r33-status__dot r33-status__dot--error" : "r33-status__dot";
|
|
266
|
+
const codeLine = isError && safeCode.length > 0 ? `<p class="r33-code">Error code: <span class="r33-code__value">${safeCode}</span></p>` : "";
|
|
267
|
+
return `<!doctype html>
|
|
268
|
+
<html lang="en" data-theme="dark">
|
|
269
|
+
<head>
|
|
270
|
+
<meta charset="utf-8">
|
|
271
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
272
|
+
<title>${title} \xB7 route33.ai</title>
|
|
273
|
+
<style>${PAGE_STYLE}</style>
|
|
274
|
+
</head>
|
|
275
|
+
<body>
|
|
276
|
+
<main class="r33-page">
|
|
277
|
+
<div class="r33-panel">
|
|
278
|
+
${BRAND_MARK}
|
|
279
|
+
<span class="r33-status"><span class="${dotClass}"></span>${statusLabel}</span>
|
|
280
|
+
<h1 class="r33-headline">${headline}</h1>
|
|
281
|
+
<p class="r33-subline">${subline}</p>
|
|
282
|
+
${codeLine}
|
|
283
|
+
</div>
|
|
284
|
+
</main>
|
|
285
|
+
</body>
|
|
286
|
+
</html>`;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/auth/loopback-callback/loopback-callback.ts
|
|
290
|
+
var createAuthLoopbackCallback = async () => {
|
|
291
|
+
let redirectUri = "http://127.0.0.1/callback";
|
|
292
|
+
let resolveCapture;
|
|
293
|
+
const captured = new Promise((resolve) => {
|
|
294
|
+
resolveCapture = resolve;
|
|
295
|
+
});
|
|
296
|
+
const server = createServer((request, response) => {
|
|
297
|
+
const url = new URL(request.url ?? "/", redirectUri);
|
|
298
|
+
if (request.method !== "GET" || url.pathname !== "/callback") {
|
|
299
|
+
response.writeHead(404).end();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const resolve = resolveCapture;
|
|
303
|
+
resolveCapture = void 0;
|
|
304
|
+
resolve?.(url);
|
|
305
|
+
const error = url.searchParams.get("error");
|
|
306
|
+
const page = error === null || error.length === 0 ? buildCallbackPage("success") : buildCallbackPage("error", error);
|
|
307
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
308
|
+
response.end(page);
|
|
309
|
+
});
|
|
310
|
+
await new Promise((resolve, reject) => {
|
|
311
|
+
const onError = (cause) => {
|
|
312
|
+
reject(new AuthError("AUTH_CALLBACK_LISTEN_FAILED", { cause }));
|
|
313
|
+
};
|
|
314
|
+
server.once("error", onError);
|
|
315
|
+
server.listen(0, "127.0.0.1", () => {
|
|
316
|
+
server.off("error", onError);
|
|
317
|
+
resolve();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
const address = server.address();
|
|
321
|
+
if (address === null || typeof address === "string") {
|
|
322
|
+
server.close();
|
|
323
|
+
throw new AuthError("AUTH_CALLBACK_LISTEN_FAILED");
|
|
324
|
+
}
|
|
325
|
+
redirectUri = `http://127.0.0.1:${String(address.port)}/callback`;
|
|
326
|
+
return {
|
|
327
|
+
redirectUri,
|
|
328
|
+
close: () => {
|
|
329
|
+
if (!server.listening) {
|
|
330
|
+
return Promise.resolve();
|
|
331
|
+
}
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
server.close((cause) => {
|
|
334
|
+
if (cause === void 0) {
|
|
335
|
+
resolve();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
reject(new AuthError("AUTH_CALLBACK_CLEANUP_FAILED", { cause }));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
wait: () => captured
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/auth/open-browser-and-capture/open-browser-and-capture.ts
|
|
347
|
+
var openBrowserAndCapture = async (options) => {
|
|
348
|
+
const callback = await createAuthLoopbackCallback();
|
|
349
|
+
const authorizationUrl = options.authorizationUrl(callback.redirectUri);
|
|
350
|
+
const browserRunner = options.browserRunner ?? ((url) => new Promise((resolve, reject) => {
|
|
351
|
+
let command = "xdg-open";
|
|
352
|
+
let args = [url.href];
|
|
353
|
+
if (process.platform === "darwin") {
|
|
354
|
+
command = "open";
|
|
355
|
+
} else if (process.platform === "win32") {
|
|
356
|
+
command = "cmd";
|
|
357
|
+
args = ["/c", "start", "", url.href];
|
|
358
|
+
}
|
|
359
|
+
const child = spawn(command, args, {
|
|
360
|
+
detached: true,
|
|
361
|
+
stdio: "ignore"
|
|
362
|
+
});
|
|
363
|
+
child.once("error", reject);
|
|
364
|
+
child.once("spawn", () => {
|
|
365
|
+
child.unref();
|
|
366
|
+
resolve();
|
|
367
|
+
});
|
|
368
|
+
}));
|
|
369
|
+
let timer;
|
|
370
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
371
|
+
timer = setTimeout(() => {
|
|
372
|
+
reject(new AuthError("AUTH_CALLBACK_TIMEOUT"));
|
|
373
|
+
}, options.timeoutMs ?? 12e4);
|
|
374
|
+
});
|
|
375
|
+
const launch = browserRunner(authorizationUrl).catch((cause) => {
|
|
376
|
+
throw new AuthError("AUTH_BROWSER_LAUNCH_FAILED", { cause });
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
const completed = await Promise.race([
|
|
380
|
+
Promise.all([launch, callback.wait()]),
|
|
381
|
+
timeout
|
|
382
|
+
]);
|
|
383
|
+
const callbackUrl = completed[1];
|
|
384
|
+
const actualState = callbackUrl.searchParams.get("state");
|
|
385
|
+
if (actualState === null) {
|
|
386
|
+
throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
|
|
387
|
+
}
|
|
388
|
+
const expectedState = Buffer.from(options.expectedState);
|
|
389
|
+
const receivedState = Buffer.from(actualState);
|
|
390
|
+
if (expectedState.length !== receivedState.length || !timingSafeEqual(expectedState, receivedState)) {
|
|
391
|
+
throw new AuthError("AUTH_CALLBACK_STATE_MISMATCH");
|
|
392
|
+
}
|
|
393
|
+
const code = callbackUrl.searchParams.get("code");
|
|
394
|
+
if (code === null || code.length === 0) {
|
|
395
|
+
throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
|
|
396
|
+
}
|
|
397
|
+
return { code, redirectUri: callback.redirectUri };
|
|
398
|
+
} finally {
|
|
399
|
+
if (timer !== void 0) {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
}
|
|
402
|
+
await callback.close();
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
|
|
407
|
+
import { randomBytes } from "node:crypto";
|
|
408
|
+
|
|
409
|
+
// src/auth/pkce-challenge/pkce-challenge.ts
|
|
410
|
+
import { createHash } from "node:crypto";
|
|
411
|
+
var pkceChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
|
|
412
|
+
|
|
413
|
+
// src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
|
|
414
|
+
var createPkceVerifierAndChallenge = () => {
|
|
415
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
416
|
+
return {
|
|
417
|
+
codeChallenge: pkceChallenge(codeVerifier),
|
|
418
|
+
codeVerifier,
|
|
419
|
+
state: randomBytes(32).toString("base64url")
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/auth/requested-scopes/requested-scopes.ts
|
|
424
|
+
var REQUESTED_OAUTH_SCOPES = "openid offline_access projects:read api-keys:read api-keys:create api-keys:rotate api-keys:delete";
|
|
425
|
+
|
|
426
|
+
// src/auth/login-flow/login-flow.ts
|
|
427
|
+
var loginFlow = async (options) => {
|
|
428
|
+
const log = options.logger ?? logger;
|
|
429
|
+
log.info("auth.login.started");
|
|
430
|
+
const pkce = createPkceVerifierAndChallenge();
|
|
431
|
+
const captured = await openBrowserAndCapture({
|
|
432
|
+
authorizationUrl: (redirectUri) => options.session.authorizationUrl({
|
|
433
|
+
clientId: options.clientId,
|
|
434
|
+
codeChallenge: pkce.codeChallenge,
|
|
435
|
+
redirectUri,
|
|
436
|
+
scope: REQUESTED_OAUTH_SCOPES,
|
|
437
|
+
state: pkce.state
|
|
438
|
+
}),
|
|
439
|
+
...options.browserRunner === void 0 ? {} : { browserRunner: options.browserRunner },
|
|
440
|
+
expectedState: pkce.state,
|
|
441
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
442
|
+
});
|
|
443
|
+
const tokenSet = await options.session.exchangeCode({
|
|
444
|
+
code: captured.code,
|
|
445
|
+
codeVerifier: pkce.codeVerifier,
|
|
446
|
+
redirectUri: captured.redirectUri
|
|
447
|
+
});
|
|
448
|
+
let credential;
|
|
449
|
+
try {
|
|
450
|
+
credential = parseManagementJwt(tokenSet.access_token);
|
|
451
|
+
} catch (cause) {
|
|
452
|
+
throw new AuthError("AUTH_TOKEN_RESPONSE_INVALID", { cause });
|
|
453
|
+
}
|
|
454
|
+
log.info("auth.login.completed");
|
|
455
|
+
return { credential, tokenSet };
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// src/key-lease/default-gateway-key-renewal-ratio/default-gateway-key-renewal-ratio.ts
|
|
459
|
+
var DEFAULT_GATEWAY_KEY_RENEWAL_RATIO = 0.8;
|
|
460
|
+
|
|
461
|
+
// src/key-lease/default-gateway-key-ttl-ms/default-gateway-key-ttl-ms.ts
|
|
462
|
+
var DEFAULT_GATEWAY_KEY_TTL_MS = 15 * 60 * 1e3;
|
|
463
|
+
|
|
464
|
+
// src/key-lease/key-lease-error/key-lease-error.ts
|
|
465
|
+
var REDACTED_CAUSE = "Route 33 gateway key lease dependency failed";
|
|
466
|
+
var KeyLeaseError = class extends Error {
|
|
467
|
+
constructor(code, options) {
|
|
468
|
+
super(
|
|
469
|
+
"Route 33 gateway key lease failed",
|
|
470
|
+
options?.cause === void 0 ? void 0 : { cause: REDACTED_CAUSE }
|
|
471
|
+
);
|
|
472
|
+
this.code = code;
|
|
473
|
+
this.name = "KeyLeaseError";
|
|
474
|
+
}
|
|
475
|
+
code;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/key-lease/map-renewal-failure/map-renewal-failure.ts
|
|
479
|
+
var mapRenewalFailure = (options) => {
|
|
480
|
+
if (options.current === void 0) {
|
|
481
|
+
throw options.cause;
|
|
482
|
+
}
|
|
483
|
+
if (options.now < Date.parse(options.current.expiresAt)) {
|
|
484
|
+
throw options.cause;
|
|
485
|
+
}
|
|
486
|
+
if (!(options.cause instanceof KeyLeaseError)) {
|
|
487
|
+
throw options.cause;
|
|
488
|
+
}
|
|
489
|
+
if (options.cause.code !== "KEY_LEASE_MINT_FAILED") {
|
|
490
|
+
throw options.cause;
|
|
491
|
+
}
|
|
492
|
+
throw new KeyLeaseError("KEY_LEASE_EXPIRED", { cause: options.cause });
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// src/key-lease/orphan-control/orphan-control.ts
|
|
496
|
+
var controlOrphans = async (options) => {
|
|
497
|
+
const now = options.clock();
|
|
498
|
+
if (!Number.isFinite(now) || !Number.isInteger(options.retentionCycles) || options.retentionCycles <= 0 || !Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
|
499
|
+
return { kind: "unavailable" };
|
|
500
|
+
}
|
|
501
|
+
let keys;
|
|
502
|
+
try {
|
|
503
|
+
keys = await options.managementClient.list(options.managementCredential);
|
|
504
|
+
} catch {
|
|
505
|
+
return { kind: "unavailable" };
|
|
506
|
+
}
|
|
507
|
+
const staleKeys = keys.data.filter(
|
|
508
|
+
(key) => key.name === options.keyName && key.id !== options.currentKeyId && Date.parse(key.expires_at) <= now
|
|
509
|
+
);
|
|
510
|
+
const deletions = await Promise.allSettled(
|
|
511
|
+
staleKeys.map(
|
|
512
|
+
(key) => Promise.resolve().then(
|
|
513
|
+
() => options.managementClient.delete(options.managementCredential, key.id)
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
return {
|
|
518
|
+
attempted: deletions.length,
|
|
519
|
+
deleted: deletions.filter(({ status }) => status === "fulfilled").length,
|
|
520
|
+
kind: "completed"
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/key-lease/is-management-unauthorized/is-management-unauthorized.ts
|
|
525
|
+
var isManagementUnauthorized = (cause) => {
|
|
526
|
+
if (cause instanceof Response) {
|
|
527
|
+
return cause.status === 401;
|
|
528
|
+
}
|
|
529
|
+
if (typeof cause !== "object") {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
if (cause === null) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
if ("status" in cause && cause.status === 401) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if ("statusCode" in cause && cause.statusCode === 401) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
return "response" in cause && cause.response instanceof Response && cause.response.status === 401;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/key-lease/recover-lease-state/recover-lease-state.ts
|
|
545
|
+
var recoverLeaseState = async (options) => {
|
|
546
|
+
let keys;
|
|
547
|
+
try {
|
|
548
|
+
keys = (await options.managementClient.list(options.managementCredential)).data;
|
|
549
|
+
} catch (cause) {
|
|
550
|
+
throw new KeyLeaseError(
|
|
551
|
+
isManagementUnauthorized(cause) ? "KEY_LEASE_UNAUTHORIZED" : "KEY_LEASE_MINT_FAILED",
|
|
552
|
+
{ cause }
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const matches = keys.filter((key2) => {
|
|
556
|
+
if (key2.name !== options.keyName || key2.revoked || key2.status !== "active" || !Number.isFinite(Date.parse(key2.expires_at))) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
switch (options.attribution.kind) {
|
|
560
|
+
case "personal":
|
|
561
|
+
return key2.ownership_type === "user_personal" && key2.project_id === null;
|
|
562
|
+
case "project":
|
|
563
|
+
return key2.ownership_type === "project" && key2.project_id === options.attribution.projectId;
|
|
564
|
+
default: {
|
|
565
|
+
const unexpected = options.attribution;
|
|
566
|
+
throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
const key = matches.length === 1 ? matches[0] : void 0;
|
|
571
|
+
if (key === void 0) {
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
const recovered = {
|
|
575
|
+
attribution: options.attribution,
|
|
576
|
+
expiresAt: key.expires_at,
|
|
577
|
+
keyId: key.id
|
|
578
|
+
};
|
|
579
|
+
return options.state.compareAndSwap(void 0, recovered) ? recovered : options.state.snapshot();
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/key-lease/reject-lease/reject-lease.ts
|
|
583
|
+
var rejectLease = async (options) => {
|
|
584
|
+
const shouldClear = options.storedKey === void 0 || options.storedKey.value === options.rejection.key.value;
|
|
585
|
+
if (shouldClear) {
|
|
586
|
+
try {
|
|
587
|
+
await options.secretStore.set(void 0);
|
|
588
|
+
} catch (cause) {
|
|
589
|
+
throw new KeyLeaseError("KEY_LEASE_STORE_UNAVAILABLE", { cause });
|
|
590
|
+
}
|
|
591
|
+
if (!options.state.clear(options.current)) {
|
|
592
|
+
throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
switch (options.rejection.kind) {
|
|
596
|
+
case "expired":
|
|
597
|
+
throw new KeyLeaseError("KEY_LEASE_EXPIRED");
|
|
598
|
+
case "unauthorized":
|
|
599
|
+
throw new KeyLeaseError("KEY_LEASE_UNAUTHORIZED");
|
|
600
|
+
default: {
|
|
601
|
+
const unexpected = options.rejection;
|
|
602
|
+
throw new TypeError(`Unexpected lease rejection: ${String(unexpected)}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/key-lease/resolve-key-ownership/resolve-key-ownership.ts
|
|
608
|
+
var resolveKeyOwnership = (options) => {
|
|
609
|
+
switch (options.attribution.kind) {
|
|
610
|
+
case "personal":
|
|
611
|
+
if (options.managementSubject.length === 0) {
|
|
612
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
ownerId: options.managementSubject,
|
|
616
|
+
ownerType: "user_personal",
|
|
617
|
+
projectId: null
|
|
618
|
+
};
|
|
619
|
+
case "project":
|
|
620
|
+
return {
|
|
621
|
+
ownerId: null,
|
|
622
|
+
ownerType: "project",
|
|
623
|
+
projectId: options.attribution.projectId
|
|
624
|
+
};
|
|
625
|
+
default: {
|
|
626
|
+
const unexpected = options.attribution;
|
|
627
|
+
throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/key-lease/mint-key/mint-key.ts
|
|
633
|
+
var mintKey = async (options) => {
|
|
634
|
+
const now = options.clock();
|
|
635
|
+
if (!Number.isFinite(now)) {
|
|
636
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
637
|
+
}
|
|
638
|
+
if (!Number.isFinite(options.installation.ttlMs)) {
|
|
639
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
640
|
+
}
|
|
641
|
+
if (options.installation.ttlMs <= 0) {
|
|
642
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
643
|
+
}
|
|
644
|
+
if (options.installation.keyName.length === 0) {
|
|
645
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
646
|
+
}
|
|
647
|
+
const ownership = resolveKeyOwnership({
|
|
648
|
+
attribution: options.attribution,
|
|
649
|
+
managementSubject: options.identity.subject
|
|
650
|
+
});
|
|
651
|
+
let material;
|
|
652
|
+
try {
|
|
653
|
+
material = await options.managementClient.create(
|
|
654
|
+
options.identity.credential,
|
|
655
|
+
{
|
|
656
|
+
expiresAt: new Date(now + options.installation.ttlMs).toISOString(),
|
|
657
|
+
name: options.installation.keyName,
|
|
658
|
+
...ownership
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
} catch (cause) {
|
|
662
|
+
if (isManagementUnauthorized(cause)) {
|
|
663
|
+
throw new KeyLeaseError("KEY_LEASE_UNAUTHORIZED", { cause });
|
|
664
|
+
}
|
|
665
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED", { cause });
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
key: material.key,
|
|
669
|
+
lease: {
|
|
670
|
+
attribution: options.attribution,
|
|
671
|
+
expiresAt: material.expires_at,
|
|
672
|
+
keyId: material.id
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// src/key-lease/renew-key/renew-key.ts
|
|
678
|
+
var renewals = /* @__PURE__ */ new WeakMap();
|
|
679
|
+
var renewKey = async (options) => {
|
|
680
|
+
const inFlight = renewals.get(options.leaseState);
|
|
681
|
+
if (inFlight !== void 0) {
|
|
682
|
+
return inFlight;
|
|
683
|
+
}
|
|
684
|
+
const expected = options.leaseState.snapshot();
|
|
685
|
+
const renewal = (async () => {
|
|
686
|
+
const discard = async (keyId) => {
|
|
687
|
+
const cleared = options.leaseState.clear(expected);
|
|
688
|
+
await Promise.allSettled([
|
|
689
|
+
Promise.resolve().then(() => options.secretStore.set(void 0)),
|
|
690
|
+
Promise.resolve().then(
|
|
691
|
+
() => options.mint.managementClient.delete(
|
|
692
|
+
options.mint.identity.credential,
|
|
693
|
+
keyId
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
]);
|
|
697
|
+
return cleared;
|
|
698
|
+
};
|
|
699
|
+
let renewed;
|
|
700
|
+
if (expected === void 0) {
|
|
701
|
+
renewed = await mintKey(options.mint);
|
|
702
|
+
} else {
|
|
703
|
+
try {
|
|
704
|
+
const material = await options.mint.managementClient.regenerate(
|
|
705
|
+
options.mint.identity.credential,
|
|
706
|
+
expected.keyId,
|
|
707
|
+
new Date(
|
|
708
|
+
options.mint.clock() + options.mint.installation.ttlMs
|
|
709
|
+
).toISOString()
|
|
710
|
+
);
|
|
711
|
+
renewed = {
|
|
712
|
+
key: material.key,
|
|
713
|
+
lease: {
|
|
714
|
+
attribution: options.mint.attribution,
|
|
715
|
+
expiresAt: material.expires_at,
|
|
716
|
+
keyId: expected.keyId
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
} catch (cause) {
|
|
720
|
+
const cleared = await discard(expected.keyId);
|
|
721
|
+
if (!cleared) {
|
|
722
|
+
throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE", { cause });
|
|
723
|
+
}
|
|
724
|
+
throw new KeyLeaseError(
|
|
725
|
+
isManagementUnauthorized(cause) ? "KEY_LEASE_UNAUTHORIZED" : "KEY_LEASE_MINT_FAILED",
|
|
726
|
+
{ cause }
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
await options.secretStore.set(renewed.key);
|
|
732
|
+
} catch (cause) {
|
|
733
|
+
const cleared = await discard(renewed.lease.keyId);
|
|
734
|
+
if (!cleared) {
|
|
735
|
+
throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE", { cause });
|
|
736
|
+
}
|
|
737
|
+
throw new KeyLeaseError("KEY_LEASE_STORE_UNAVAILABLE", { cause });
|
|
738
|
+
}
|
|
739
|
+
if (!options.leaseState.compareAndSwap(expected, renewed.lease)) {
|
|
740
|
+
await discard(renewed.lease.keyId);
|
|
741
|
+
throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE");
|
|
742
|
+
}
|
|
743
|
+
return renewed;
|
|
744
|
+
})();
|
|
745
|
+
renewals.set(options.leaseState, renewal);
|
|
746
|
+
try {
|
|
747
|
+
return await renewal;
|
|
748
|
+
} finally {
|
|
749
|
+
if (renewals.get(options.leaseState) === renewal) {
|
|
750
|
+
renewals.delete(options.leaseState);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/key-lease/resolve-lease-policy/resolve-lease-policy.ts
|
|
756
|
+
var resolveLeasePolicy = (policy) => {
|
|
757
|
+
const resolved = {
|
|
758
|
+
deletePreviousKey: false,
|
|
759
|
+
renewalRatio: DEFAULT_GATEWAY_KEY_RENEWAL_RATIO,
|
|
760
|
+
ttlMs: DEFAULT_GATEWAY_KEY_TTL_MS,
|
|
761
|
+
...policy
|
|
762
|
+
};
|
|
763
|
+
if (!Number.isFinite(resolved.ttlMs)) {
|
|
764
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
765
|
+
}
|
|
766
|
+
if (resolved.ttlMs <= 0) {
|
|
767
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
768
|
+
}
|
|
769
|
+
if (!Number.isFinite(resolved.renewalRatio)) {
|
|
770
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
771
|
+
}
|
|
772
|
+
if (resolved.renewalRatio <= 0) {
|
|
773
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
774
|
+
}
|
|
775
|
+
if (resolved.renewalRatio >= 1) {
|
|
776
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
777
|
+
}
|
|
778
|
+
if (resolved.orphanRetentionCycles === void 0) {
|
|
779
|
+
return resolved;
|
|
780
|
+
}
|
|
781
|
+
if (!Number.isInteger(resolved.orphanRetentionCycles)) {
|
|
782
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
783
|
+
}
|
|
784
|
+
if (resolved.orphanRetentionCycles <= 0) {
|
|
785
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
786
|
+
}
|
|
787
|
+
return resolved;
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// src/key-lease/resolve-seam-value/resolve-seam-value.ts
|
|
791
|
+
var resolveSeamValue = async (value) => typeof value === "function" ? value() : value;
|
|
792
|
+
|
|
793
|
+
// src/key-lease/reusable-lease/reusable-lease.ts
|
|
794
|
+
var reusableLease = (options) => {
|
|
795
|
+
if (options.current === void 0) {
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
if (options.storedKey === void 0) {
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
if (!options.state.matchesAttribution(options.attribution)) {
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
804
|
+
const renewalAt = Date.parse(options.current.expiresAt) - options.policy.ttlMs * (1 - options.policy.renewalRatio);
|
|
805
|
+
if (options.now >= renewalAt) {
|
|
806
|
+
return void 0;
|
|
807
|
+
}
|
|
808
|
+
return { key: options.storedKey, lease: options.current };
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// src/key-lease/lease-for-attribution/lease-for-attribution.ts
|
|
812
|
+
var leaseForAttribution = async (options) => {
|
|
813
|
+
const policy = resolveLeasePolicy(options.policy);
|
|
814
|
+
const clock = options.clock ?? Date.now;
|
|
815
|
+
const now = clock();
|
|
816
|
+
if (!Number.isFinite(now)) {
|
|
817
|
+
throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
|
|
818
|
+
}
|
|
819
|
+
let current = options.installation.state.snapshot();
|
|
820
|
+
let storedKey;
|
|
821
|
+
try {
|
|
822
|
+
storedKey = await options.installation.secretStore.get();
|
|
823
|
+
} catch (cause) {
|
|
824
|
+
throw new KeyLeaseError("KEY_LEASE_STORE_UNAVAILABLE", { cause });
|
|
825
|
+
}
|
|
826
|
+
if (current === void 0 && storedKey !== void 0) {
|
|
827
|
+
current = await recoverLeaseState({
|
|
828
|
+
attribution: options.attribution,
|
|
829
|
+
keyName: options.installation.keyName,
|
|
830
|
+
managementClient: options.management.client,
|
|
831
|
+
managementCredential: await resolveSeamValue(
|
|
832
|
+
options.management.credential
|
|
833
|
+
),
|
|
834
|
+
state: options.installation.state
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
if (options.rejection !== void 0) {
|
|
838
|
+
return rejectLease({
|
|
839
|
+
current,
|
|
840
|
+
rejection: options.rejection,
|
|
841
|
+
secretStore: options.installation.secretStore,
|
|
842
|
+
state: options.installation.state,
|
|
843
|
+
storedKey
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
const reusable = reusableLease({
|
|
847
|
+
attribution: options.attribution,
|
|
848
|
+
current,
|
|
849
|
+
now,
|
|
850
|
+
policy,
|
|
851
|
+
state: options.installation.state,
|
|
852
|
+
storedKey
|
|
853
|
+
});
|
|
854
|
+
if (reusable !== void 0) {
|
|
855
|
+
return reusable;
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
const renewed = await renewKey({
|
|
859
|
+
leaseState: options.installation.state,
|
|
860
|
+
mint: {
|
|
861
|
+
attribution: options.attribution,
|
|
862
|
+
clock,
|
|
863
|
+
identity: {
|
|
864
|
+
credential: await resolveSeamValue(options.management.credential),
|
|
865
|
+
subject: await resolveSeamValue(options.management.subject)
|
|
866
|
+
},
|
|
867
|
+
installation: {
|
|
868
|
+
keyName: options.installation.keyName,
|
|
869
|
+
ttlMs: policy.ttlMs
|
|
870
|
+
},
|
|
871
|
+
managementClient: options.management.client
|
|
872
|
+
},
|
|
873
|
+
secretStore: options.installation.secretStore
|
|
874
|
+
});
|
|
875
|
+
if (policy.orphanRetentionCycles !== void 0) {
|
|
876
|
+
await controlOrphans({
|
|
877
|
+
clock,
|
|
878
|
+
currentKeyId: renewed.lease.keyId,
|
|
879
|
+
keyName: options.installation.keyName,
|
|
880
|
+
managementClient: options.management.client,
|
|
881
|
+
managementCredential: await resolveSeamValue(
|
|
882
|
+
options.management.credential
|
|
883
|
+
),
|
|
884
|
+
retentionCycles: policy.orphanRetentionCycles,
|
|
885
|
+
ttlMs: policy.ttlMs
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
return renewed;
|
|
889
|
+
} catch (cause) {
|
|
890
|
+
return mapRenewalFailure({ cause, current, now });
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// src/attribution/parse-attribution/parse-attribution.ts
|
|
895
|
+
var parseAttribution = (value) => {
|
|
896
|
+
if (!isRecord(value)) {
|
|
897
|
+
throw new ContractParseError("invalid attribution");
|
|
898
|
+
}
|
|
899
|
+
if (value["kind"] === "personal") {
|
|
900
|
+
return { kind: "personal" };
|
|
901
|
+
}
|
|
902
|
+
if (value["kind"] === "project" && typeof value["projectId"] === "string") {
|
|
903
|
+
return { kind: "project", projectId: value["projectId"] };
|
|
904
|
+
}
|
|
905
|
+
throw new ContractParseError("invalid attribution");
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/key-lease/lease-state/lease-state.ts
|
|
909
|
+
var LeaseState = class _LeaseState {
|
|
910
|
+
/** Current committed snapshot; mutation is limited to compare-and-swap transitions. */
|
|
911
|
+
#current;
|
|
912
|
+
constructor(current) {
|
|
913
|
+
this.#current = current;
|
|
914
|
+
}
|
|
915
|
+
/** Restores untrusted persisted metadata or an empty lease.
|
|
916
|
+
* @param value - Persisted metadata, or undefined for a new installation.
|
|
917
|
+
* @returns A lease-state coordinator.
|
|
918
|
+
*/
|
|
919
|
+
static restore(value) {
|
|
920
|
+
if (value === void 0) {
|
|
921
|
+
return new _LeaseState(void 0);
|
|
922
|
+
}
|
|
923
|
+
if (!isRecord(value)) {
|
|
924
|
+
throw new ContractParseError("invalid gateway key lease state");
|
|
925
|
+
}
|
|
926
|
+
const fields = Object.keys(value);
|
|
927
|
+
if (fields.length !== 3 || fields.some(
|
|
928
|
+
(field) => field !== "attribution" && field !== "expiresAt" && field !== "keyId"
|
|
929
|
+
)) {
|
|
930
|
+
throw new ContractParseError("invalid gateway key lease state");
|
|
931
|
+
}
|
|
932
|
+
const expiresAt = value["expiresAt"];
|
|
933
|
+
const keyId = value["keyId"];
|
|
934
|
+
if (typeof expiresAt !== "string" || !Number.isFinite(Date.parse(expiresAt)) || typeof keyId !== "string" || keyId.length === 0) {
|
|
935
|
+
throw new ContractParseError("invalid gateway key lease state");
|
|
936
|
+
}
|
|
937
|
+
return new _LeaseState({
|
|
938
|
+
attribution: parseAttribution(value["attribution"]),
|
|
939
|
+
expiresAt,
|
|
940
|
+
keyId
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/** Reads the current non-secret metadata.
|
|
944
|
+
* @returns The current lease metadata, if one is committed.
|
|
945
|
+
*/
|
|
946
|
+
snapshot() {
|
|
947
|
+
return this.#current;
|
|
948
|
+
}
|
|
949
|
+
/** Checks whether current metadata belongs to an attribution.
|
|
950
|
+
* @param attribution - The requested ownership choice.
|
|
951
|
+
* @returns Whether the current lease has the same attribution.
|
|
952
|
+
*/
|
|
953
|
+
matchesAttribution(attribution) {
|
|
954
|
+
if (this.#current === void 0) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
switch (this.#current.attribution.kind) {
|
|
958
|
+
case "personal":
|
|
959
|
+
return attribution.kind === "personal";
|
|
960
|
+
case "project":
|
|
961
|
+
return attribution.kind === "project" && attribution.projectId === this.#current.attribution.projectId;
|
|
962
|
+
default:
|
|
963
|
+
return _LeaseState.#unexpectedAttribution(this.#current.attribution);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/** Atomically swaps metadata when the expected snapshot is still current.
|
|
967
|
+
* @param expected - Snapshot observed before asynchronous renewal.
|
|
968
|
+
* @param next - Fresh metadata to commit.
|
|
969
|
+
* @returns Whether the swap committed.
|
|
970
|
+
*/
|
|
971
|
+
compareAndSwap(expected, next) {
|
|
972
|
+
if (this.#current !== expected) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
this.#current = next;
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
/** Clears metadata only while the expected snapshot remains current.
|
|
979
|
+
* @param expected - Snapshot whose lease is being invalidated.
|
|
980
|
+
* @returns Whether the state was cleared.
|
|
981
|
+
*/
|
|
982
|
+
clear(expected) {
|
|
983
|
+
if (this.#current !== expected) {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
this.#current = void 0;
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
static #unexpectedAttribution(value) {
|
|
990
|
+
throw new TypeError(`Unexpected attribution: ${String(value)}`);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// src/contract/management-keys/parse-api-key/parse-api-key.ts
|
|
995
|
+
var parseApiKey = (value) => {
|
|
996
|
+
if (!isRecord(value) || !isOwnerType(value["ownership_type"]) || !isNullableString(value["project_id"])) {
|
|
997
|
+
throw new ContractParseError("invalid API key");
|
|
998
|
+
}
|
|
999
|
+
const field = (name) => {
|
|
1000
|
+
const text = value[name];
|
|
1001
|
+
if (typeof text !== "string") {
|
|
1002
|
+
throw new ContractParseError("invalid API key");
|
|
1003
|
+
}
|
|
1004
|
+
return text;
|
|
1005
|
+
};
|
|
1006
|
+
const status = value["status"];
|
|
1007
|
+
if (status !== "active" && status !== "revoked") {
|
|
1008
|
+
throw new ContractParseError("invalid API key");
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
expires_at: field("expires_at"),
|
|
1012
|
+
id: field("id"),
|
|
1013
|
+
name: field("name"),
|
|
1014
|
+
org_id: field("org_id"),
|
|
1015
|
+
ownership_type: value["ownership_type"],
|
|
1016
|
+
project_id: value["project_id"],
|
|
1017
|
+
revoked: value["revoked"] === true,
|
|
1018
|
+
status
|
|
1019
|
+
};
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// src/contract/management-keys/parse-collection/parse-collection.ts
|
|
1023
|
+
var parseCollection = (value, parseItem) => {
|
|
1024
|
+
if (!isRecord(value)) {
|
|
1025
|
+
throw new ContractParseError("invalid management collection");
|
|
1026
|
+
}
|
|
1027
|
+
const data = value["data"];
|
|
1028
|
+
if (!Array.isArray(data)) {
|
|
1029
|
+
throw new ContractParseError("invalid management collection");
|
|
1030
|
+
}
|
|
1031
|
+
const page = value["page"];
|
|
1032
|
+
if (!isRecord(page)) {
|
|
1033
|
+
throw new ContractParseError("invalid management collection");
|
|
1034
|
+
}
|
|
1035
|
+
const limit = page["limit"];
|
|
1036
|
+
if (typeof limit !== "number") {
|
|
1037
|
+
throw new ContractParseError("invalid management collection");
|
|
1038
|
+
}
|
|
1039
|
+
if (![Number.isInteger(limit), limit >= 1, limit <= 500].every(Boolean)) {
|
|
1040
|
+
throw new ContractParseError("invalid management collection");
|
|
1041
|
+
}
|
|
1042
|
+
const nextCursor = page["next_cursor"];
|
|
1043
|
+
if (!isNullableString(nextCursor)) {
|
|
1044
|
+
throw new ContractParseError("invalid management collection");
|
|
1045
|
+
}
|
|
1046
|
+
const hasMore = page["has_more"];
|
|
1047
|
+
if (typeof hasMore !== "boolean") {
|
|
1048
|
+
throw new ContractParseError("invalid management collection");
|
|
1049
|
+
}
|
|
1050
|
+
const collection = {
|
|
1051
|
+
data: data.map(parseItem),
|
|
1052
|
+
page: {
|
|
1053
|
+
has_more: hasMore,
|
|
1054
|
+
limit,
|
|
1055
|
+
next_cursor: nextCursor
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
const meta = value["meta"];
|
|
1059
|
+
if (meta === void 0) {
|
|
1060
|
+
return collection;
|
|
1061
|
+
}
|
|
1062
|
+
if (!isRecord(meta)) {
|
|
1063
|
+
throw new ContractParseError("invalid management collection");
|
|
1064
|
+
}
|
|
1065
|
+
return { ...collection, meta };
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
// src/contract/management-keys/parse-key-material/parse-key-material.ts
|
|
1069
|
+
var parseKeyMaterial = (value) => {
|
|
1070
|
+
if (!isRecord(value) || typeof value["key"] !== "string") {
|
|
1071
|
+
throw new ContractParseError("invalid key material");
|
|
1072
|
+
}
|
|
1073
|
+
return { ...parseApiKey(value), key: new GatewayApiKey(value["key"]) };
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// src/contract/management-keys/parse-organization/parse-organization.ts
|
|
1077
|
+
var parseOrganization = (value) => {
|
|
1078
|
+
if (!isRecord(value)) {
|
|
1079
|
+
throw new ContractParseError("invalid organization");
|
|
1080
|
+
}
|
|
1081
|
+
const stringField = (name) => {
|
|
1082
|
+
const field = value[name];
|
|
1083
|
+
if (typeof field !== "string") {
|
|
1084
|
+
throw new ContractParseError("invalid organization");
|
|
1085
|
+
}
|
|
1086
|
+
return field;
|
|
1087
|
+
};
|
|
1088
|
+
const nullableStringField = (name) => {
|
|
1089
|
+
const field = value[name];
|
|
1090
|
+
if (!isNullableString(field)) {
|
|
1091
|
+
throw new ContractParseError("invalid organization");
|
|
1092
|
+
}
|
|
1093
|
+
return field;
|
|
1094
|
+
};
|
|
1095
|
+
const numberField = (name) => {
|
|
1096
|
+
const field = value[name];
|
|
1097
|
+
if (typeof field !== "number") {
|
|
1098
|
+
throw new ContractParseError("invalid organization");
|
|
1099
|
+
}
|
|
1100
|
+
return field;
|
|
1101
|
+
};
|
|
1102
|
+
const nullableNumberField = (name) => {
|
|
1103
|
+
const field = value[name];
|
|
1104
|
+
if (typeof field !== "number" && field !== null) {
|
|
1105
|
+
throw new ContractParseError("invalid organization");
|
|
1106
|
+
}
|
|
1107
|
+
return field;
|
|
1108
|
+
};
|
|
1109
|
+
const booleanField = (name) => {
|
|
1110
|
+
const field = value[name];
|
|
1111
|
+
if (typeof field !== "boolean") {
|
|
1112
|
+
throw new ContractParseError("invalid organization");
|
|
1113
|
+
}
|
|
1114
|
+
return field;
|
|
1115
|
+
};
|
|
1116
|
+
const budgetField = (name) => {
|
|
1117
|
+
const field = value[name];
|
|
1118
|
+
if (field === null) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
if (!isRecord(field) || typeof field["amount"] !== "string" || typeof field["currency"] !== "string") {
|
|
1122
|
+
throw new ContractParseError("invalid organization");
|
|
1123
|
+
}
|
|
1124
|
+
return { amount: field["amount"], currency: field["currency"] };
|
|
1125
|
+
};
|
|
1126
|
+
const setupStatus = value["setup_status"];
|
|
1127
|
+
if (setupStatus !== "broken" && setupStatus !== "ok" && setupStatus !== "partial" && setupStatus !== "unvalidated") {
|
|
1128
|
+
throw new ContractParseError("invalid organization");
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
alert_active: booleanField("alert_active"),
|
|
1132
|
+
alert_percentage: numberField("alert_percentage"),
|
|
1133
|
+
azure_group_id: nullableStringField("azure_group_id"),
|
|
1134
|
+
azure_owners_group_id: nullableStringField("azure_owners_group_id"),
|
|
1135
|
+
created_at: nullableStringField("created_at"),
|
|
1136
|
+
deactivated_at: nullableStringField("deactivated_at"),
|
|
1137
|
+
default_project_budget: budgetField("default_project_budget"),
|
|
1138
|
+
default_user_budget: budgetField("default_user_budget"),
|
|
1139
|
+
domain: nullableStringField("domain"),
|
|
1140
|
+
id: stringField("id"),
|
|
1141
|
+
idp_connection: nullableStringField("idp_connection"),
|
|
1142
|
+
member_count: nullableNumberField("member_count"),
|
|
1143
|
+
name: stringField("name"),
|
|
1144
|
+
scim_sync: nullableStringField("scim_sync"),
|
|
1145
|
+
setup_status: setupStatus,
|
|
1146
|
+
slug: nullableStringField("slug"),
|
|
1147
|
+
tenant_id: nullableStringField("tenant_id"),
|
|
1148
|
+
updated_at: nullableStringField("updated_at")
|
|
1149
|
+
};
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// src/contract/management-keys/parse-project/parse-project.ts
|
|
1153
|
+
var parseProject = (value) => {
|
|
1154
|
+
if (!isRecord(value) || !hasStringFields(value, ["id", "name"]) || !hasStringFields(value, ["id", "name", "org_id"])) {
|
|
1155
|
+
throw new ContractParseError("invalid project");
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
id: value["id"] ?? "",
|
|
1159
|
+
name: value["name"] ?? "",
|
|
1160
|
+
org_id: value["org_id"] ?? ""
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
// src/management/management-error/management-error.ts
|
|
1165
|
+
var ManagementError = class extends Error {
|
|
1166
|
+
constructor(code, options) {
|
|
1167
|
+
super(
|
|
1168
|
+
"Route 33 management request failed",
|
|
1169
|
+
options?.cause === void 0 ? void 0 : { cause: "Route 33 management dependency failed" }
|
|
1170
|
+
);
|
|
1171
|
+
this.code = code;
|
|
1172
|
+
this.name = "ManagementError";
|
|
1173
|
+
this.statusCode = options?.statusCode;
|
|
1174
|
+
}
|
|
1175
|
+
code;
|
|
1176
|
+
statusCode;
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// src/management/management-http-transport/management-http-transport.ts
|
|
1180
|
+
var ManagementHttpTransport = class {
|
|
1181
|
+
#baseUrl;
|
|
1182
|
+
#credential;
|
|
1183
|
+
#fetchImpl;
|
|
1184
|
+
#onUnauthorized;
|
|
1185
|
+
#timeoutMs;
|
|
1186
|
+
constructor(options) {
|
|
1187
|
+
const baseUrl = options.baseUrl ?? "https://api.route33.ai/v1";
|
|
1188
|
+
this.#baseUrl = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
1189
|
+
this.#credential = options.credential;
|
|
1190
|
+
this.#fetchImpl = options.fetchImpl ?? fetch;
|
|
1191
|
+
this.#onUnauthorized = options.onUnauthorized;
|
|
1192
|
+
this.#timeoutMs = options.timeoutMs ?? 1e4;
|
|
1193
|
+
}
|
|
1194
|
+
/** Sends a request and reads one JSON value under the request timeout.
|
|
1195
|
+
* @param request - Management path, method, and optional JSON body.
|
|
1196
|
+
* @returns The untrusted response value.
|
|
1197
|
+
*/
|
|
1198
|
+
json(request) {
|
|
1199
|
+
return this.#perform(request, async (response) => {
|
|
1200
|
+
try {
|
|
1201
|
+
return await response.json();
|
|
1202
|
+
} catch (cause) {
|
|
1203
|
+
throw new ContractParseError("invalid management response", { cause });
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
/** Sends a request whose successful response has no body.
|
|
1208
|
+
* @param request - Management path and method.
|
|
1209
|
+
*/
|
|
1210
|
+
empty(request) {
|
|
1211
|
+
return this.#perform(request, () => Promise.resolve());
|
|
1212
|
+
}
|
|
1213
|
+
async #perform(request, consume) {
|
|
1214
|
+
const controller = new AbortController();
|
|
1215
|
+
const timer = setTimeout(() => {
|
|
1216
|
+
controller.abort();
|
|
1217
|
+
}, this.#timeoutMs);
|
|
1218
|
+
const headers = new Headers({
|
|
1219
|
+
accept: "application/json",
|
|
1220
|
+
authorization: `Bearer ${this.#credential.value}`
|
|
1221
|
+
});
|
|
1222
|
+
if (request.body !== void 0) {
|
|
1223
|
+
headers.set("content-type", "application/json");
|
|
1224
|
+
}
|
|
1225
|
+
try {
|
|
1226
|
+
const send = () => this.#fetchImpl(new URL(request.path, this.#baseUrl), {
|
|
1227
|
+
...request.body === void 0 ? {} : { body: JSON.stringify(request.body) },
|
|
1228
|
+
headers,
|
|
1229
|
+
method: request.method,
|
|
1230
|
+
signal: controller.signal
|
|
1231
|
+
});
|
|
1232
|
+
let response = await send();
|
|
1233
|
+
if (response.status === 401 && this.#onUnauthorized !== void 0) {
|
|
1234
|
+
await this.#onUnauthorized();
|
|
1235
|
+
response = await send();
|
|
1236
|
+
}
|
|
1237
|
+
if (!response.ok) {
|
|
1238
|
+
throw new ManagementError(
|
|
1239
|
+
response.status === 401 ? "MANAGEMENT_UNAUTHORIZED" : "MANAGEMENT_HTTP_ERROR",
|
|
1240
|
+
{ statusCode: response.status }
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return await consume(response);
|
|
1244
|
+
} catch (cause) {
|
|
1245
|
+
if (cause instanceof ManagementError) {
|
|
1246
|
+
throw cause;
|
|
1247
|
+
}
|
|
1248
|
+
if (controller.signal.aborted) {
|
|
1249
|
+
throw new ManagementError("MANAGEMENT_TIMEOUT", { cause });
|
|
1250
|
+
}
|
|
1251
|
+
if (cause instanceof ContractParseError) {
|
|
1252
|
+
throw cause;
|
|
1253
|
+
}
|
|
1254
|
+
throw new ManagementError("MANAGEMENT_NETWORK_ERROR", { cause });
|
|
1255
|
+
} finally {
|
|
1256
|
+
clearTimeout(timer);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
// src/management/http-management-keys/http-management-keys.ts
|
|
1262
|
+
var HttpManagementKeys = class {
|
|
1263
|
+
#credential;
|
|
1264
|
+
#transport;
|
|
1265
|
+
constructor(options) {
|
|
1266
|
+
this.#credential = options.credential;
|
|
1267
|
+
this.#transport = new ManagementHttpTransport(options);
|
|
1268
|
+
}
|
|
1269
|
+
/** Creates a future-expiring ownership-scoped key.
|
|
1270
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1271
|
+
* @param input - Key name, expiry, and ownership.
|
|
1272
|
+
* @returns One-time key material.
|
|
1273
|
+
*/
|
|
1274
|
+
async create(accessToken, input) {
|
|
1275
|
+
this.#authorize(accessToken);
|
|
1276
|
+
return parseKeyMaterial(
|
|
1277
|
+
await this.#transport.json({
|
|
1278
|
+
body: {
|
|
1279
|
+
expires_at: input.expiresAt,
|
|
1280
|
+
name: input.name,
|
|
1281
|
+
...input.ownerId === null ? {} : { owner_user_id: input.ownerId },
|
|
1282
|
+
owner_type: input.ownerType,
|
|
1283
|
+
...input.projectId === null ? {} : { project_id: input.projectId }
|
|
1284
|
+
},
|
|
1285
|
+
method: "POST",
|
|
1286
|
+
path: "api-keys"
|
|
1287
|
+
})
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
/** Treats a successful empty response as confirmation of permanent removal.
|
|
1291
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1292
|
+
* @param id - Key identifier.
|
|
1293
|
+
*/
|
|
1294
|
+
async delete(accessToken, id) {
|
|
1295
|
+
this.#authorize(accessToken);
|
|
1296
|
+
await this.#transport.empty({
|
|
1297
|
+
method: "DELETE",
|
|
1298
|
+
path: `api-keys/${encodeURIComponent(id)}`
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
/** Gets a safe key DTO.
|
|
1302
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1303
|
+
* @param id - Key identifier.
|
|
1304
|
+
* @returns The secret-free key DTO.
|
|
1305
|
+
*/
|
|
1306
|
+
async get(accessToken, id) {
|
|
1307
|
+
this.#authorize(accessToken);
|
|
1308
|
+
return parseApiKey(
|
|
1309
|
+
await this.#transport.json({
|
|
1310
|
+
method: "GET",
|
|
1311
|
+
path: `api-keys/${encodeURIComponent(id)}`
|
|
1312
|
+
})
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
/** Lists safe key DTOs.
|
|
1316
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1317
|
+
* @returns The parsed key collection.
|
|
1318
|
+
*/
|
|
1319
|
+
async list(accessToken) {
|
|
1320
|
+
this.#authorize(accessToken);
|
|
1321
|
+
return parseCollection(
|
|
1322
|
+
await this.#transport.json({ method: "GET", path: "api-keys" }),
|
|
1323
|
+
parseApiKey
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
/** Lists organizations visible to the authenticated management subject.
|
|
1327
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1328
|
+
* @returns The parsed organization collection.
|
|
1329
|
+
*/
|
|
1330
|
+
async listOrganizations(accessToken) {
|
|
1331
|
+
this.#authorize(accessToken);
|
|
1332
|
+
return parseCollection(
|
|
1333
|
+
await this.#transport.json({ method: "GET", path: "orgs" }),
|
|
1334
|
+
parseOrganization
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
/** Lists projects visible within an organisation filter.
|
|
1338
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1339
|
+
* @param organisationId - Required backend organisation filter.
|
|
1340
|
+
* @returns The parsed project collection.
|
|
1341
|
+
*/
|
|
1342
|
+
async listProjects(accessToken, organisationId) {
|
|
1343
|
+
this.#authorize(accessToken);
|
|
1344
|
+
if (organisationId.trim().length === 0) {
|
|
1345
|
+
throw new ManagementError("MANAGEMENT_ORGANISATION_ID_REQUIRED", {
|
|
1346
|
+
statusCode: 400
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
const query = new URLSearchParams({ org_id: organisationId });
|
|
1350
|
+
return parseCollection(
|
|
1351
|
+
await this.#transport.json({
|
|
1352
|
+
method: "GET",
|
|
1353
|
+
path: `projects?${query.toString()}`
|
|
1354
|
+
}),
|
|
1355
|
+
parseProject
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
/** Regenerates one-time key material while preserving expiry when omitted.
|
|
1359
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1360
|
+
* @param id - Key identifier.
|
|
1361
|
+
* @param expiresAt - Optional replacement expiry.
|
|
1362
|
+
* @returns Regenerated one-time key material.
|
|
1363
|
+
*/
|
|
1364
|
+
async regenerate(accessToken, id, expiresAt) {
|
|
1365
|
+
this.#authorize(accessToken);
|
|
1366
|
+
return parseKeyMaterial(
|
|
1367
|
+
await this.#transport.json({
|
|
1368
|
+
body: expiresAt === void 0 ? {} : { expires_at: expiresAt },
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
path: `api-keys/${encodeURIComponent(id)}/secrets`
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
/** Updates mutable key metadata.
|
|
1375
|
+
* @param accessToken - Credential required by the frozen contract.
|
|
1376
|
+
* @param id - Key identifier.
|
|
1377
|
+
* @param input - Optional name and expiry replacements.
|
|
1378
|
+
* @returns The updated safe key DTO.
|
|
1379
|
+
*/
|
|
1380
|
+
async update(accessToken, id, input) {
|
|
1381
|
+
this.#authorize(accessToken);
|
|
1382
|
+
return parseApiKey(
|
|
1383
|
+
await this.#transport.json({
|
|
1384
|
+
body: {
|
|
1385
|
+
...input.expiresAt === void 0 ? {} : { expires_at: input.expiresAt },
|
|
1386
|
+
...input.name === void 0 ? {} : { name: input.name }
|
|
1387
|
+
},
|
|
1388
|
+
method: "PATCH",
|
|
1389
|
+
path: `api-keys/${encodeURIComponent(id)}`
|
|
1390
|
+
})
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
#authorize(accessToken) {
|
|
1394
|
+
if (accessToken.value !== this.#credential.value) {
|
|
1395
|
+
throw new ManagementError("MANAGEMENT_UNAUTHORIZED", {
|
|
1396
|
+
statusCode: 401
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/cli/production-smoke-error/production-smoke-error.ts
|
|
1403
|
+
var ProductionSmokeError = class extends Error {
|
|
1404
|
+
constructor(code, options) {
|
|
1405
|
+
super(
|
|
1406
|
+
"Route 33 production smoke failed",
|
|
1407
|
+
options?.cause === void 0 ? void 0 : { cause: "Route 33 production smoke dependency failed" }
|
|
1408
|
+
);
|
|
1409
|
+
this.code = code;
|
|
1410
|
+
this.name = "ProductionSmokeError";
|
|
1411
|
+
this.statusCode = options?.statusCode;
|
|
1412
|
+
}
|
|
1413
|
+
code;
|
|
1414
|
+
statusCode;
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
// src/cli/parse-management-subject/parse-management-subject.ts
|
|
1418
|
+
var parseManagementSubjectForSmoke = (credential) => {
|
|
1419
|
+
try {
|
|
1420
|
+
return parseManagementSubject(credential);
|
|
1421
|
+
} catch (cause) {
|
|
1422
|
+
throw new ProductionSmokeError("SMOKE_TOKEN_INVALID", { cause });
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/cli/precheck-route33-auth/precheck-route33-auth.ts
|
|
1427
|
+
var precheckRoute33Auth = async (options) => {
|
|
1428
|
+
const controller = new AbortController();
|
|
1429
|
+
const timer = setTimeout(() => {
|
|
1430
|
+
controller.abort();
|
|
1431
|
+
}, options.timeoutMs ?? 15e3);
|
|
1432
|
+
const authorizeUrl = new URL("/authorize", options.issuerUrl);
|
|
1433
|
+
const { codeChallenge } = createPkceVerifierAndChallenge();
|
|
1434
|
+
authorizeUrl.search = new URLSearchParams({
|
|
1435
|
+
client_id: options.clientId,
|
|
1436
|
+
code_challenge: codeChallenge,
|
|
1437
|
+
code_challenge_method: "S256",
|
|
1438
|
+
redirect_uri: "http://127.0.0.1:9/callback",
|
|
1439
|
+
response_type: "code",
|
|
1440
|
+
scope: "openid",
|
|
1441
|
+
state: "smoke"
|
|
1442
|
+
}).toString();
|
|
1443
|
+
try {
|
|
1444
|
+
const response = await (options.fetchImpl ?? fetch)(authorizeUrl, {
|
|
1445
|
+
redirect: "manual",
|
|
1446
|
+
signal: controller.signal
|
|
1447
|
+
});
|
|
1448
|
+
if (response.status < 200 || response.status >= 400) {
|
|
1449
|
+
throw new ProductionSmokeError("SMOKE_AUTH_PRECHECK_FAILED", {
|
|
1450
|
+
statusCode: response.status
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
} catch (cause) {
|
|
1454
|
+
if (cause instanceof ProductionSmokeError) {
|
|
1455
|
+
throw cause;
|
|
1456
|
+
}
|
|
1457
|
+
throw new ProductionSmokeError("SMOKE_AUTH_PRECHECK_FAILED", { cause });
|
|
1458
|
+
} finally {
|
|
1459
|
+
clearTimeout(timer);
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// src/cli/create-live-production-smoke-plan/create-live-production-smoke-plan.ts
|
|
1464
|
+
var createLiveProductionSmokePlan = (options) => {
|
|
1465
|
+
const state = LeaseState.restore(void 0);
|
|
1466
|
+
const keyIds = /* @__PURE__ */ new Set();
|
|
1467
|
+
let session;
|
|
1468
|
+
let secret;
|
|
1469
|
+
let mintedReceipt;
|
|
1470
|
+
const secretStore = {
|
|
1471
|
+
get: () => Promise.resolve(secret),
|
|
1472
|
+
set: (next) => {
|
|
1473
|
+
secret = next;
|
|
1474
|
+
return Promise.resolve();
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
const requireSession = () => {
|
|
1478
|
+
if (session === void 0) {
|
|
1479
|
+
throw new ProductionSmokeError("SMOKE_SESSION_UNAVAILABLE");
|
|
1480
|
+
}
|
|
1481
|
+
return session;
|
|
1482
|
+
};
|
|
1483
|
+
return {
|
|
1484
|
+
precheck: () => options.precheck?.() ?? precheckRoute33Auth({
|
|
1485
|
+
clientId: options.clientId,
|
|
1486
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
|
|
1487
|
+
issuerUrl: options.authIssuerUrl
|
|
1488
|
+
}),
|
|
1489
|
+
login: async () => {
|
|
1490
|
+
const result = options.login === void 0 ? await loginFlow({
|
|
1491
|
+
clientId: options.clientId,
|
|
1492
|
+
logger,
|
|
1493
|
+
session: createAuthSession({
|
|
1494
|
+
clientId: options.clientId,
|
|
1495
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
|
|
1496
|
+
issuerUrl: options.authIssuerUrl
|
|
1497
|
+
}),
|
|
1498
|
+
timeoutMs: 12e4
|
|
1499
|
+
}) : await options.login();
|
|
1500
|
+
session = {
|
|
1501
|
+
client: new HttpManagementKeys({
|
|
1502
|
+
baseUrl: options.apiBaseUrl,
|
|
1503
|
+
credential: result.credential,
|
|
1504
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1505
|
+
}),
|
|
1506
|
+
credential: result.credential,
|
|
1507
|
+
subject: parseManagementSubjectForSmoke(result.credential)
|
|
1508
|
+
};
|
|
1509
|
+
},
|
|
1510
|
+
projects: async () => {
|
|
1511
|
+
const active = requireSession();
|
|
1512
|
+
const organizations = await active.client.listOrganizations(
|
|
1513
|
+
active.credential
|
|
1514
|
+
);
|
|
1515
|
+
const projects = await Promise.all(
|
|
1516
|
+
organizations.data.map(async (organization) => {
|
|
1517
|
+
const collection = await active.client.listProjects(
|
|
1518
|
+
active.credential,
|
|
1519
|
+
organization.id
|
|
1520
|
+
);
|
|
1521
|
+
return collection.data.map((project) => ({
|
|
1522
|
+
...project,
|
|
1523
|
+
organisation: { id: organization.id, name: organization.name }
|
|
1524
|
+
}));
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
return projects.flat();
|
|
1528
|
+
},
|
|
1529
|
+
mint: async () => {
|
|
1530
|
+
const active = requireSession();
|
|
1531
|
+
const minted = await leaseForAttribution({
|
|
1532
|
+
attribution: { kind: "personal" },
|
|
1533
|
+
...options.clock === void 0 ? {} : { clock: options.clock },
|
|
1534
|
+
installation: {
|
|
1535
|
+
keyName: "route33-production-smoke",
|
|
1536
|
+
secretStore,
|
|
1537
|
+
state
|
|
1538
|
+
},
|
|
1539
|
+
management: active,
|
|
1540
|
+
policy: { ttlMs: 36e5 }
|
|
1541
|
+
});
|
|
1542
|
+
mintedReceipt = {
|
|
1543
|
+
expiresAt: minted.lease.expiresAt,
|
|
1544
|
+
keyId: minted.lease.keyId
|
|
1545
|
+
};
|
|
1546
|
+
keyIds.add(minted.lease.keyId);
|
|
1547
|
+
return mintedReceipt;
|
|
1548
|
+
},
|
|
1549
|
+
rotate: async () => {
|
|
1550
|
+
const active = requireSession();
|
|
1551
|
+
if (mintedReceipt === void 0) {
|
|
1552
|
+
throw new ProductionSmokeError("SMOKE_SESSION_UNAVAILABLE");
|
|
1553
|
+
}
|
|
1554
|
+
const regenerated = await active.client.regenerate(
|
|
1555
|
+
active.credential,
|
|
1556
|
+
mintedReceipt.keyId
|
|
1557
|
+
);
|
|
1558
|
+
await secretStore.set(regenerated.key);
|
|
1559
|
+
keyIds.add(regenerated.id);
|
|
1560
|
+
const oldKey = (await active.client.list(active.credential)).data.find(
|
|
1561
|
+
({ id }) => id === mintedReceipt?.keyId
|
|
1562
|
+
);
|
|
1563
|
+
if (oldKey === void 0 || oldKey.expires_at !== mintedReceipt.expiresAt || !Number.isFinite(Date.parse(oldKey.expires_at))) {
|
|
1564
|
+
throw new ProductionSmokeError("SMOKE_ROTATION_INVALID");
|
|
1565
|
+
}
|
|
1566
|
+
return { expiresAt: regenerated.expires_at, keyId: regenerated.id };
|
|
1567
|
+
},
|
|
1568
|
+
cleanup: async () => {
|
|
1569
|
+
const active = session;
|
|
1570
|
+
const results = active === void 0 ? [] : await Promise.allSettled(
|
|
1571
|
+
[...keyIds].map(
|
|
1572
|
+
(id) => active.client.delete(active.credential, id)
|
|
1573
|
+
)
|
|
1574
|
+
);
|
|
1575
|
+
await secretStore.set(void 0);
|
|
1576
|
+
if (results.some(({ status }) => status === "rejected")) {
|
|
1577
|
+
throw new ProductionSmokeError("SMOKE_CLEANUP_FAILED");
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
// src/cli/read-env-client-id/read-env-client-id.ts
|
|
1584
|
+
var readEnvClientId = (env = process.env) => {
|
|
1585
|
+
const value = env["ROUTE33_CLIENT_ID"]?.trim();
|
|
1586
|
+
return value === "" ? void 0 : value;
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/cli/production-smoke/production-smoke.ts
|
|
1590
|
+
var productionSmokeClientId = readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID;
|
|
1591
|
+
var ProductionSmoke = class {
|
|
1592
|
+
#plan;
|
|
1593
|
+
#write;
|
|
1594
|
+
constructor(options = {}) {
|
|
1595
|
+
const apiBaseUrl = process.env["ROUTE33_SMOKE_API_BASE"]?.trim();
|
|
1596
|
+
const authIssuerUrl = process.env["ROUTE33_SMOKE_AUTH_ISSUER"]?.trim();
|
|
1597
|
+
this.#plan = options.plan ?? createLiveProductionSmokePlan({
|
|
1598
|
+
apiBaseUrl: apiBaseUrl === void 0 || apiBaseUrl.length === 0 ? "https://mapi.route33.ai/api/v1" : apiBaseUrl,
|
|
1599
|
+
authIssuerUrl: authIssuerUrl === void 0 || authIssuerUrl.length === 0 ? "https://auth.route33.ai" : authIssuerUrl,
|
|
1600
|
+
clientId: productionSmokeClientId
|
|
1601
|
+
});
|
|
1602
|
+
this.#write = options.write ?? ((line) => {
|
|
1603
|
+
logger.info(line);
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
/** Runs each smoke step in its declared order. */
|
|
1607
|
+
async run() {
|
|
1608
|
+
await this.#step("PRECHECK", () => this.#plan.precheck());
|
|
1609
|
+
await this.#step("LOGIN", () => this.#plan.login());
|
|
1610
|
+
const projects = await this.#step("PROJECTS", () => this.#plan.projects());
|
|
1611
|
+
for (const project of projects) {
|
|
1612
|
+
this.#write(`${project.name} (${project.organisation.name})`);
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
await this.#step(
|
|
1616
|
+
"MINT",
|
|
1617
|
+
() => this.#plan.mint(),
|
|
1618
|
+
({ expiresAt, keyId }) => `key_id=${keyId} expires_at=${expiresAt}`
|
|
1619
|
+
);
|
|
1620
|
+
await this.#step(
|
|
1621
|
+
"ROTATE",
|
|
1622
|
+
() => this.#plan.rotate(),
|
|
1623
|
+
({ expiresAt, keyId }) => `key_id=${keyId} expires_at=${expiresAt}`
|
|
1624
|
+
);
|
|
1625
|
+
} finally {
|
|
1626
|
+
await this.#step("CLEANUP", () => this.#plan.cleanup());
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async #step(name, run, details) {
|
|
1630
|
+
try {
|
|
1631
|
+
const value = await run();
|
|
1632
|
+
const suffix = details === void 0 ? "" : ` ${details(value)}`;
|
|
1633
|
+
this.#write(`${name} PASS${suffix}`);
|
|
1634
|
+
return value;
|
|
1635
|
+
} catch (cause) {
|
|
1636
|
+
this.#write(`${name} FAIL`);
|
|
1637
|
+
throw cause;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
1642
|
+
void new ProductionSmoke().run().catch(() => {
|
|
1643
|
+
logger.error("production-smoke.failed");
|
|
1644
|
+
process.exitCode = 1;
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
export {
|
|
1648
|
+
ProductionSmoke
|
|
1649
|
+
};
|