@openparachute/hub 0.5.7 → 0.5.10-rc.10
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/package.json +1 -1
- package/src/__tests__/admin-clients.test.ts +275 -0
- package/src/__tests__/admin-handlers.test.ts +70 -323
- package/src/__tests__/admin-host-admin-token.test.ts +52 -4
- package/src/__tests__/api-me.test.ts +149 -0
- package/src/__tests__/api-mint-token.test.ts +381 -0
- package/src/__tests__/api-modules-ops.test.ts +658 -0
- package/src/__tests__/api-modules.test.ts +426 -0
- package/src/__tests__/api-revocation-list.test.ts +198 -0
- package/src/__tests__/api-revoke-token.test.ts +320 -0
- package/src/__tests__/api-tokens.test.ts +629 -0
- package/src/__tests__/auth.test.ts +680 -16
- package/src/__tests__/csrf.test.ts +40 -1
- package/src/__tests__/expose-2fa-warning.test.ts +3 -5
- package/src/__tests__/expose-cloudflare.test.ts +1 -1
- package/src/__tests__/expose.test.ts +2 -2
- package/src/__tests__/hub-server.test.ts +584 -67
- package/src/__tests__/hub-settings.test.ts +377 -0
- package/src/__tests__/hub.test.ts +123 -53
- package/src/__tests__/install-source.test.ts +249 -0
- package/src/__tests__/jwt-sign.test.ts +205 -0
- package/src/__tests__/module-manifest.test.ts +48 -0
- package/src/__tests__/oauth-handlers.test.ts +522 -5
- package/src/__tests__/operator-token.test.ts +427 -3
- package/src/__tests__/origin-check.test.ts +220 -0
- package/src/__tests__/request-protocol.test.ts +54 -0
- package/src/__tests__/serve-boot.test.ts +193 -0
- package/src/__tests__/serve.test.ts +100 -0
- package/src/__tests__/sessions.test.ts +25 -2
- package/src/__tests__/setup-gate.test.ts +222 -0
- package/src/__tests__/setup-wizard.test.ts +2089 -0
- package/src/__tests__/status.test.ts +199 -0
- package/src/__tests__/supervisor.test.ts +482 -0
- package/src/__tests__/upgrade.test.ts +247 -4
- package/src/__tests__/vault-name.test.ts +79 -0
- package/src/__tests__/well-known.test.ts +69 -0
- package/src/admin-clients.ts +139 -0
- package/src/admin-handlers.ts +37 -254
- package/src/admin-host-admin-token.ts +25 -10
- package/src/admin-login-ui.ts +256 -0
- package/src/admin-vault-admin-token.ts +1 -1
- package/src/api-me.ts +124 -0
- package/src/api-mint-token.ts +239 -0
- package/src/api-modules-ops.ts +585 -0
- package/src/api-modules.ts +367 -0
- package/src/api-revocation-list.ts +59 -0
- package/src/api-revoke-token.ts +153 -0
- package/src/api-tokens.ts +224 -0
- package/src/cli.ts +28 -0
- package/src/commands/auth.ts +408 -51
- package/src/commands/expose-2fa-warning.ts +6 -6
- package/src/commands/serve-boot.ts +133 -0
- package/src/commands/serve.ts +214 -0
- package/src/commands/status.ts +74 -10
- package/src/commands/upgrade.ts +33 -6
- package/src/csrf.ts +34 -13
- package/src/help.ts +55 -5
- package/src/hub-control.ts +1 -0
- package/src/hub-db.ts +87 -0
- package/src/hub-server.ts +767 -136
- package/src/hub-settings.ts +259 -0
- package/src/hub.ts +298 -150
- package/src/install-source.ts +291 -0
- package/src/jwt-sign.ts +265 -5
- package/src/module-manifest.ts +48 -10
- package/src/oauth-handlers.ts +262 -56
- package/src/oauth-ui.ts +23 -2
- package/src/operator-token.ts +349 -18
- package/src/origin-check.ts +127 -0
- package/src/rate-limit.ts +5 -2
- package/src/request-protocol.ts +48 -0
- package/src/scope-explanations.ts +33 -2
- package/src/sessions.ts +30 -18
- package/src/setup-wizard.ts +2009 -0
- package/src/supervisor.ts +411 -0
- package/src/vault-name.ts +71 -0
- package/src/well-known.ts +54 -1
- package/web/ui/dist/assets/index-BDSEsaBY.css +1 -0
- package/web/ui/dist/assets/index-CP07NbdF.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/admin-config.test.ts +0 -281
- package/src/admin-config-ui.ts +0 -534
- package/src/admin-config.ts +0 -226
- package/web/ui/dist/assets/index-BKzPDdB0.js +0 -60
- package/web/ui/dist/assets/index-Dyk6g7vT.css +0 -1
package/src/api-me.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/me` — public who-am-I endpoint for hub-served surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Reads the `parachute_hub_session` cookie. If present and active, returns
|
|
5
|
+
* the user identity AND a CSRF token bound to the existing CSRF cookie (or
|
|
6
|
+
* a freshly-minted one). Otherwise returns the minimal `{ hasSession: false }`
|
|
7
|
+
* payload.
|
|
8
|
+
*
|
|
9
|
+
* Public — no auth required (returns "not signed in" rather than 401 when
|
|
10
|
+
* no session, so the SPA / discovery page can render a consistent affordance
|
|
11
|
+
* regardless of auth state). No CORS — same-origin only; hub-served UIs
|
|
12
|
+
* are same-origin.
|
|
13
|
+
*
|
|
14
|
+
* Response shape:
|
|
15
|
+
*
|
|
16
|
+
* { hasSession: false }
|
|
17
|
+
* { hasSession: true, user: { id, displayName }, csrf: "<token>" }
|
|
18
|
+
*
|
|
19
|
+
* `displayName` is the user's `username` today — there's no separate
|
|
20
|
+
* display-name field on the User shape. Surfaced under a different key
|
|
21
|
+
* here so a future profile-name migration can land without breaking
|
|
22
|
+
* SPA / discovery consumers.
|
|
23
|
+
*
|
|
24
|
+
* Why include the CSRF token only when signed in: there's nothing to
|
|
25
|
+
* sign-out (or otherwise mutate) without a session. Minting a token in
|
|
26
|
+
* the unsigned-in case would just bloat the response and prime a cookie
|
|
27
|
+
* the consumer has no use for.
|
|
28
|
+
*
|
|
29
|
+
* Why a dedicated endpoint rather than probing a session-gated SPA page:
|
|
30
|
+
* those redirect to /login when unauthenticated, which is exactly the
|
|
31
|
+
* wrong UX for an unconditionally-fetched "show sign-in affordance" call.
|
|
32
|
+
* `/api/me` cleanly returns either state without a bounce.
|
|
33
|
+
*
|
|
34
|
+
* The CSRF token returned here is the same token any same-session
|
|
35
|
+
* `<form>` would carry — the consumer (SPA fetch POST or
|
|
36
|
+
* server-rendered discovery form) submits it back as `__csrf` against
|
|
37
|
+
* the existing logout / mutation handlers.
|
|
38
|
+
*/
|
|
39
|
+
import type { Database } from "bun:sqlite";
|
|
40
|
+
import { ensureCsrfToken } from "./csrf.ts";
|
|
41
|
+
import { findActiveSession } from "./sessions.ts";
|
|
42
|
+
import { getUserById } from "./users.ts";
|
|
43
|
+
|
|
44
|
+
export interface ApiMeDeps {
|
|
45
|
+
db: Database;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SignedInUser {
|
|
49
|
+
id: string;
|
|
50
|
+
displayName: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Discriminated union mirroring the client-side `MeResponse` shape in
|
|
55
|
+
* `web/ui/src/lib/api.ts`. The two early returns below (no-session,
|
|
56
|
+
* deleted-user) construct the `false` arm; the success path constructs
|
|
57
|
+
* the `true` arm. Typing it as a union (rather than an interface with
|
|
58
|
+
* optional fields) means the compiler refuses any future construction
|
|
59
|
+
* that mixes states — e.g. `{ hasSession: false, user: staleUser }`
|
|
60
|
+
* fails at the type-check, not just at code-review.
|
|
61
|
+
*/
|
|
62
|
+
type ApiMeResponse = { hasSession: false } | { hasSession: true; user: SignedInUser; csrf: string };
|
|
63
|
+
|
|
64
|
+
export function handleApiMe(req: Request, deps: ApiMeDeps): Response {
|
|
65
|
+
if (req.method !== "GET") {
|
|
66
|
+
return jsonError(405, "method_not_allowed", "use GET");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const session = findActiveSession(deps.db, req);
|
|
70
|
+
if (!session) {
|
|
71
|
+
return ok({ hasSession: false });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const user = getUserById(deps.db, session.userId);
|
|
75
|
+
if (!user) {
|
|
76
|
+
// Session row points at a deleted user — treat as not signed in.
|
|
77
|
+
// The session row should be cleaned up by some future sweep, but
|
|
78
|
+
// surfacing a stale identity to the UI would be worse than a
|
|
79
|
+
// momentary "signed out" affordance.
|
|
80
|
+
return ok({ hasSession: false });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mint a CSRF token (or reuse the existing cookie's). When this is the
|
|
84
|
+
// first request to set the cookie, attach Set-Cookie so the browser
|
|
85
|
+
// stores it for future logout submission.
|
|
86
|
+
const csrf = ensureCsrfToken(req);
|
|
87
|
+
const headers: Record<string, string> = {
|
|
88
|
+
"content-type": "application/json",
|
|
89
|
+
// Don't cache — session can change at any time (login, logout,
|
|
90
|
+
// expiry). The endpoint is cheap; revalidate on every request.
|
|
91
|
+
"cache-control": "no-store",
|
|
92
|
+
};
|
|
93
|
+
if (csrf.setCookie) headers["set-cookie"] = csrf.setCookie;
|
|
94
|
+
|
|
95
|
+
const body: ApiMeResponse = {
|
|
96
|
+
hasSession: true,
|
|
97
|
+
user: {
|
|
98
|
+
id: user.id,
|
|
99
|
+
displayName: user.username,
|
|
100
|
+
},
|
|
101
|
+
csrf: csrf.token,
|
|
102
|
+
};
|
|
103
|
+
return new Response(JSON.stringify(body), { status: 200, headers });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ok(body: ApiMeResponse): Response {
|
|
107
|
+
return new Response(JSON.stringify(body), {
|
|
108
|
+
status: 200,
|
|
109
|
+
headers: {
|
|
110
|
+
"content-type": "application/json",
|
|
111
|
+
"cache-control": "no-store",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function jsonError(status: number, error: string, description: string): Response {
|
|
117
|
+
return new Response(JSON.stringify({ error, error_description: description }), {
|
|
118
|
+
status,
|
|
119
|
+
headers: {
|
|
120
|
+
"content-type": "application/json",
|
|
121
|
+
"cache-control": "no-store",
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /api/auth/mint-token` — HTTP companion to `parachute auth mint-token`.
|
|
3
|
+
*
|
|
4
|
+
* Same arg/return shape as the CLI; just the network path. Used by:
|
|
5
|
+
*
|
|
6
|
+
* - automation that doesn't have CLI access (CI runners, cloud agents)
|
|
7
|
+
* but does hold an operator-bearer with `parachute:host:auth` scope;
|
|
8
|
+
* - the future admin SPA when the operator wants to mint a one-shot
|
|
9
|
+
* scope-narrow token without dropping to a terminal.
|
|
10
|
+
*
|
|
11
|
+
* Auth: `Authorization: Bearer <token>` where `token`'s `scope` claim
|
|
12
|
+
* contains `parachute:host:auth`. The operator's local operator.token
|
|
13
|
+
* (admin scope-set) covers this; a narrow `--scope-set=auth` operator
|
|
14
|
+
* token also covers this.
|
|
15
|
+
*
|
|
16
|
+
* Why a separate endpoint instead of extending /admin/host-admin-token:
|
|
17
|
+
* that endpoint is session-cookie-gated for the SPA's needs and only
|
|
18
|
+
* mints `parachute:host:admin`. This endpoint is bearer-gated for
|
|
19
|
+
* automation and mints arbitrary scope/permissions tuples per request.
|
|
20
|
+
*
|
|
21
|
+
* Every successful mint writes a row to the `tokens` registry
|
|
22
|
+
* (`created_via='cli_mint'` — same provenance as the CLI path, since
|
|
23
|
+
* HTTP mint is just CLI-by-network). Powers the
|
|
24
|
+
* `/.well-known/parachute-revocation.json` endpoint.
|
|
25
|
+
*/
|
|
26
|
+
import type { Database } from "bun:sqlite";
|
|
27
|
+
import { inferAudience } from "./jwt-audience.ts";
|
|
28
|
+
import { recordTokenMint, signAccessToken, validateAccessToken } from "./jwt-sign.ts";
|
|
29
|
+
import { isNonRequestableScope } from "./scope-explanations.ts";
|
|
30
|
+
|
|
31
|
+
/** Default lifetime when --expires-in / `expires_in` is omitted. Matches the CLI. */
|
|
32
|
+
export const API_MINT_TOKEN_DEFAULT_TTL_SECONDS = 90 * 24 * 60 * 60;
|
|
33
|
+
/** Hard cap. Matches the CLI's --expires-in upper bound. */
|
|
34
|
+
export const API_MINT_TOKEN_MAX_TTL_SECONDS = 365 * 24 * 60 * 60;
|
|
35
|
+
/** Scope required on the bearer token to call this endpoint. */
|
|
36
|
+
export const API_MINT_TOKEN_REQUIRED_SCOPE = "parachute:host:auth";
|
|
37
|
+
/** client_id stamped on minted tokens. Matches the CLI flow's value. */
|
|
38
|
+
export const API_MINT_TOKEN_CLIENT_ID = "parachute-hub";
|
|
39
|
+
|
|
40
|
+
export interface ApiMintTokenDeps {
|
|
41
|
+
db: Database;
|
|
42
|
+
/** Hub origin — written into the JWT `iss` of minted tokens AND used to validate the bearer. */
|
|
43
|
+
issuer: string;
|
|
44
|
+
/** Test seam for time. */
|
|
45
|
+
now?: () => Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface MintTokenRequest {
|
|
49
|
+
scope?: unknown;
|
|
50
|
+
audience?: unknown;
|
|
51
|
+
expires_in?: unknown;
|
|
52
|
+
subject?: unknown;
|
|
53
|
+
permissions?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleApiMintToken(req: Request, deps: ApiMintTokenDeps): Promise<Response> {
|
|
57
|
+
if (req.method !== "POST") {
|
|
58
|
+
return jsonError(405, "method_not_allowed", "use POST");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1. Bearer presence + parsing.
|
|
62
|
+
const auth = req.headers.get("authorization");
|
|
63
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
64
|
+
return jsonError(401, "unauthenticated", "Authorization: Bearer <token> required");
|
|
65
|
+
}
|
|
66
|
+
const bearer = auth.slice("Bearer ".length).trim();
|
|
67
|
+
if (!bearer) {
|
|
68
|
+
return jsonError(401, "unauthenticated", "empty bearer token");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Bearer validation (signature, issuer, expiry, revocation).
|
|
72
|
+
let bearerSub: string;
|
|
73
|
+
let bearerScopes: string[];
|
|
74
|
+
try {
|
|
75
|
+
const validated = await validateAccessToken(deps.db, bearer, deps.issuer);
|
|
76
|
+
const sub = validated.payload.sub;
|
|
77
|
+
if (typeof sub !== "string" || sub.length === 0) {
|
|
78
|
+
return jsonError(401, "unauthenticated", "bearer token has no sub claim");
|
|
79
|
+
}
|
|
80
|
+
bearerSub = sub;
|
|
81
|
+
bearerScopes =
|
|
82
|
+
typeof validated.payload.scope === "string"
|
|
83
|
+
? validated.payload.scope.split(/\s+/).filter((s) => s.length > 0)
|
|
84
|
+
: [];
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
return jsonError(401, "unauthenticated", `bearer token invalid — ${msg}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Scope gate.
|
|
91
|
+
if (!bearerScopes.includes(API_MINT_TOKEN_REQUIRED_SCOPE)) {
|
|
92
|
+
return jsonError(
|
|
93
|
+
403,
|
|
94
|
+
"insufficient_scope",
|
|
95
|
+
`bearer token lacks ${API_MINT_TOKEN_REQUIRED_SCOPE}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Body parsing.
|
|
100
|
+
let body: MintTokenRequest;
|
|
101
|
+
try {
|
|
102
|
+
body = (await req.json()) as MintTokenRequest;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
105
|
+
return jsonError(400, "invalid_request", `body must be valid JSON — ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
if (typeof body !== "object" || body === null) {
|
|
108
|
+
return jsonError(400, "invalid_request", "body must be a JSON object");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 5. Required + typed field extraction.
|
|
112
|
+
if (typeof body.scope !== "string" || body.scope.trim().length === 0) {
|
|
113
|
+
return jsonError(400, "invalid_request", "scope is required and must be a non-empty string");
|
|
114
|
+
}
|
|
115
|
+
const scopes = body.scope.split(/\s+/).filter((s) => s.length > 0);
|
|
116
|
+
if (scopes.length === 0) {
|
|
117
|
+
return jsonError(400, "invalid_request", "scope must contain at least one scope");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Privilege-diffusion guard: mint paths cannot themselves mint tokens
|
|
121
|
+
// carrying non-requestable scopes (parachute:host:admin, the host:*
|
|
122
|
+
// narrow scopes, vault:<name>:admin). Holder of `parachute:host:auth`
|
|
123
|
+
// can mint vault/scribe/agent verb scopes for downstream services, but
|
|
124
|
+
// cannot mint another `:auth` (or any other non-requestable) without
|
|
125
|
+
// forced re-auth via the operator.token rotation path. Same set the
|
|
126
|
+
// public OAuth flow already rejects.
|
|
127
|
+
const blocked = scopes.filter((s) => isNonRequestableScope(s));
|
|
128
|
+
if (blocked.length > 0) {
|
|
129
|
+
return jsonError(
|
|
130
|
+
400,
|
|
131
|
+
"invalid_scope",
|
|
132
|
+
`scope ${blocked.join(", ")} is not requestable via mint-token; use OAuth flow or operator rotation`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let audience: string;
|
|
137
|
+
if (body.audience === undefined) {
|
|
138
|
+
audience = inferAudience(scopes);
|
|
139
|
+
} else if (typeof body.audience === "string" && body.audience.length > 0) {
|
|
140
|
+
audience = body.audience;
|
|
141
|
+
} else {
|
|
142
|
+
return jsonError(400, "invalid_request", "audience must be a non-empty string when present");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let ttlSeconds = API_MINT_TOKEN_DEFAULT_TTL_SECONDS;
|
|
146
|
+
if (body.expires_in !== undefined) {
|
|
147
|
+
if (typeof body.expires_in !== "number" || !Number.isFinite(body.expires_in)) {
|
|
148
|
+
return jsonError(400, "invalid_request", "expires_in must be a positive integer (seconds)");
|
|
149
|
+
}
|
|
150
|
+
if (!Number.isInteger(body.expires_in) || body.expires_in <= 0) {
|
|
151
|
+
return jsonError(400, "invalid_request", "expires_in must be a positive integer (seconds)");
|
|
152
|
+
}
|
|
153
|
+
if (body.expires_in > API_MINT_TOKEN_MAX_TTL_SECONDS) {
|
|
154
|
+
return jsonError(
|
|
155
|
+
400,
|
|
156
|
+
"invalid_request",
|
|
157
|
+
`expires_in exceeds 365d cap (${API_MINT_TOKEN_MAX_TTL_SECONDS} seconds)`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
ttlSeconds = body.expires_in;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let subject: string;
|
|
164
|
+
if (body.subject === undefined) {
|
|
165
|
+
subject = bearerSub;
|
|
166
|
+
} else if (typeof body.subject === "string" && body.subject.length > 0) {
|
|
167
|
+
subject = body.subject;
|
|
168
|
+
} else {
|
|
169
|
+
return jsonError(400, "invalid_request", "subject must be a non-empty string when present");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let permissionsClaim: Record<string, unknown> | undefined;
|
|
173
|
+
let permissionsCanonical: string | undefined;
|
|
174
|
+
if (body.permissions !== undefined) {
|
|
175
|
+
if (
|
|
176
|
+
typeof body.permissions !== "object" ||
|
|
177
|
+
body.permissions === null ||
|
|
178
|
+
Array.isArray(body.permissions)
|
|
179
|
+
) {
|
|
180
|
+
return jsonError(400, "invalid_request", "permissions must be a JSON object");
|
|
181
|
+
}
|
|
182
|
+
permissionsClaim = body.permissions as Record<string, unknown>;
|
|
183
|
+
permissionsCanonical = JSON.stringify(permissionsClaim);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 6. Mint + register.
|
|
187
|
+
const minted = await signAccessToken(deps.db, {
|
|
188
|
+
sub: subject,
|
|
189
|
+
scopes,
|
|
190
|
+
audience,
|
|
191
|
+
clientId: API_MINT_TOKEN_CLIENT_ID,
|
|
192
|
+
issuer: deps.issuer,
|
|
193
|
+
ttlSeconds,
|
|
194
|
+
...(permissionsClaim !== undefined ? { extraClaims: { permissions: permissionsClaim } } : {}),
|
|
195
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
recordTokenMint(deps.db, {
|
|
199
|
+
jti: minted.jti,
|
|
200
|
+
createdVia: "cli_mint",
|
|
201
|
+
subject,
|
|
202
|
+
// user_id intentionally omitted — CLI-mint rows store subject only,
|
|
203
|
+
// matching the CLI path's shape (so HTTP and CLI mints look identical
|
|
204
|
+
// in the registry). The bearer's user identity is implicit via the
|
|
205
|
+
// bearer's own user_id (which is in its own tokens row).
|
|
206
|
+
clientId: API_MINT_TOKEN_CLIENT_ID,
|
|
207
|
+
scopes,
|
|
208
|
+
expiresAt: minted.expiresAt,
|
|
209
|
+
...(permissionsCanonical !== undefined ? { permissions: permissionsCanonical } : {}),
|
|
210
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return new Response(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
jti: minted.jti,
|
|
216
|
+
token: minted.token,
|
|
217
|
+
expires_at: minted.expiresAt,
|
|
218
|
+
scope: scopes.join(" "),
|
|
219
|
+
...(permissionsClaim !== undefined ? { permissions: permissionsClaim } : {}),
|
|
220
|
+
}),
|
|
221
|
+
{
|
|
222
|
+
status: 200,
|
|
223
|
+
headers: {
|
|
224
|
+
"content-type": "application/json",
|
|
225
|
+
"cache-control": "no-store",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function jsonError(status: number, error: string, description: string): Response {
|
|
232
|
+
return new Response(JSON.stringify({ error, error_description: description }), {
|
|
233
|
+
status,
|
|
234
|
+
headers: {
|
|
235
|
+
"content-type": "application/json",
|
|
236
|
+
"cache-control": "no-store",
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|