@openparachute/hub 0.5.10-rc.6 → 0.5.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-handlers.test.ts +141 -6
- package/src/__tests__/api-account.test.ts +463 -0
- package/src/__tests__/api-modules-ops.test.ts +139 -0
- package/src/__tests__/api-modules.test.ts +134 -0
- package/src/__tests__/api-users.test.ts +522 -0
- package/src/__tests__/cors.test.ts +587 -0
- package/src/__tests__/hub-db.test.ts +126 -1
- package/src/__tests__/hub-server.test.ts +29 -4
- package/src/__tests__/hub-settings.test.ts +377 -0
- package/src/__tests__/hub.test.ts +17 -0
- package/src/__tests__/jwt-sign.test.ts +59 -0
- package/src/__tests__/oauth-handlers.test.ts +1059 -10
- package/src/__tests__/oauth-ui.test.ts +210 -0
- package/src/__tests__/scope-explanations.test.ts +23 -0
- package/src/__tests__/serve.test.ts +8 -1
- package/src/__tests__/setup-wizard.test.ts +1500 -13
- package/src/__tests__/supervisor.test.ts +76 -2
- package/src/__tests__/users.test.ts +196 -0
- package/src/__tests__/vault-name.test.ts +79 -0
- package/src/__tests__/vault-names.test.ts +172 -0
- package/src/account-change-password-ui.ts +379 -0
- package/src/admin-handlers.ts +68 -2
- package/src/admin-host-admin-token.ts +5 -0
- package/src/admin-vault-admin-token.ts +7 -0
- package/src/api-account.ts +443 -0
- package/src/api-mint-token.ts +6 -0
- package/src/api-modules-ops.ts +30 -6
- package/src/api-modules.ts +101 -0
- package/src/api-users.ts +393 -0
- package/src/commands/auth.ts +10 -1
- package/src/commands/serve.ts +5 -1
- package/src/cors.ts +263 -0
- package/src/hub-db.ts +54 -0
- package/src/hub-server.ts +162 -18
- package/src/hub-settings.ts +259 -0
- package/src/hub.ts +34 -9
- package/src/jwt-sign.ts +17 -1
- package/src/oauth-handlers.ts +256 -29
- package/src/oauth-ui.ts +451 -38
- package/src/operator-token.ts +4 -0
- package/src/scope-explanations.ts +26 -1
- package/src/setup-wizard.ts +1100 -56
- package/src/supervisor.ts +66 -14
- package/src/users.ts +210 -3
- package/src/vault-name.ts +71 -0
- package/src/vault-names.ts +57 -0
- package/web/ui/dist/assets/index-XhxYXDT5.js +61 -0
- package/web/ui/dist/assets/{index-D54otIhv.css → index-p6DkOcsk.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-AX_UHJ5e.js +0 -61
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub-local key/value settings (hub#268).
|
|
3
|
+
*
|
|
4
|
+
* Bare KV table backing two wizard-adjacent features:
|
|
5
|
+
*
|
|
6
|
+
* * `setup_expose_mode` — `localhost | tailnet | public`. The operator's
|
|
7
|
+
* "how will this hub be reached?" choice from the first-boot wizard's
|
|
8
|
+
* expose step. The done step reads it to surface the right reachable-at
|
|
9
|
+
* URL + next-step instructions.
|
|
10
|
+
*
|
|
11
|
+
* * `pending_first_client_auto_approve_until` — ISO-8601 timestamp. Set
|
|
12
|
+
* when the wizard finishes (60 minutes in the future); the next OAuth
|
|
13
|
+
* client to hit `/oauth/register` *within* that window is auto-approved
|
|
14
|
+
* (single-use, the value is cleared on consume). Past-due or absent
|
|
15
|
+
* means the standard pending-approval flow applies. Motivator: a
|
|
16
|
+
* canonical onboarding (install hub → wizard → install Notes →
|
|
17
|
+
* authorize) shouldn't bounce the operator through a manual approve
|
|
18
|
+
* step they just set up the hub for.
|
|
19
|
+
*
|
|
20
|
+
* Schema lives in `hub-db.ts` migration v7. This module is just the typed
|
|
21
|
+
* accessor — single-row reads/writes per key, no joins, no caching. The
|
|
22
|
+
* call frequency is low (a handful of reads on `/oauth/register` + the
|
|
23
|
+
* wizard's done step) so the obvious shape wins.
|
|
24
|
+
*/
|
|
25
|
+
import type { Database } from "bun:sqlite";
|
|
26
|
+
|
|
27
|
+
// Adding a setting: extend this union + write a typed accessor. The table itself is generic KV.
|
|
28
|
+
export type HubSettingKey =
|
|
29
|
+
| "setup_expose_mode"
|
|
30
|
+
| "pending_first_client_auto_approve_until"
|
|
31
|
+
// hub#272: auto-minted operator token surfaced once on the wizard's
|
|
32
|
+
// done screen. Single-use — the done-step renderer reads + deletes the
|
|
33
|
+
// row so a subsequent GET (page refresh, back button) doesn't re-show
|
|
34
|
+
// the secret. Lives in hub_settings rather than tokens because it's a
|
|
35
|
+
// wizard-flow ephemeral, not a persistent issued credential — the
|
|
36
|
+
// mintOperatorToken call still records the jti in the `tokens`
|
|
37
|
+
// registry, so revocation works as usual.
|
|
38
|
+
| "setup_minted_token"
|
|
39
|
+
// hub#267: the typed vault name. Persisted at vault POST time so the
|
|
40
|
+
// done step can render the operator's choice in the MCP URL +
|
|
41
|
+
// install-command snippet without re-deriving from services.json
|
|
42
|
+
// (vault's first-boot may write its own paths shape that the wizard
|
|
43
|
+
// can't trust to match `<name>` exactly until the spawn settles).
|
|
44
|
+
| "setup_vault_name"
|
|
45
|
+
// hub#275: which dist-tag the runtime module installer uses
|
|
46
|
+
// (`bun add -g <pkg>@<channel>`). `"latest"` (default) tracks the
|
|
47
|
+
// stable channel; `"rc"` follows the release-candidate chain so
|
|
48
|
+
// operators on the rc cadence can pull pre-release builds without
|
|
49
|
+
// hand-editing the install command. Seeded from
|
|
50
|
+
// `PARACHUTE_MODULE_CHANNEL` on first read (operator can ship a fresh
|
|
51
|
+
// box with the env var set and have the row land with their preferred
|
|
52
|
+
// channel); after the first seed the row is source of truth and the
|
|
53
|
+
// env var is ignored — admin must use the SPA toggle (or
|
|
54
|
+
// `PUT /api/modules/channel`) to change channel.
|
|
55
|
+
| "module_install_channel";
|
|
56
|
+
|
|
57
|
+
export type SetupExposeMode = "localhost" | "tailnet" | "public";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set of valid `setup_expose_mode` values. Exported so the POST handler
|
|
61
|
+
* + the wizard renderer can both reference the same truth.
|
|
62
|
+
*/
|
|
63
|
+
export const SETUP_EXPOSE_MODES: readonly SetupExposeMode[] = ["localhost", "tailnet", "public"];
|
|
64
|
+
|
|
65
|
+
export function isSetupExposeMode(s: unknown): s is SetupExposeMode {
|
|
66
|
+
return typeof s === "string" && SETUP_EXPOSE_MODES.includes(s as SetupExposeMode);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Row {
|
|
70
|
+
value: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read a setting's value, or undefined when absent. No type coercion —
|
|
75
|
+
* the caller knows what shape it expects.
|
|
76
|
+
*/
|
|
77
|
+
export function getSetting(db: Database, key: HubSettingKey): string | undefined {
|
|
78
|
+
const row = db.query<Row, [string]>("SELECT value FROM hub_settings WHERE key = ?").get(key);
|
|
79
|
+
return row?.value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Write (or overwrite) a setting. UPSERT semantics — the SQLite
|
|
84
|
+
* `ON CONFLICT(key) DO UPDATE` shape is the canonical way and works back
|
|
85
|
+
* to the hub's SQLite minimum. `updated_at` is bumped on every write,
|
|
86
|
+
* even idempotent re-writes of the same value, so an operational poll
|
|
87
|
+
* could distinguish stale vs fresh state.
|
|
88
|
+
*/
|
|
89
|
+
export function setSetting(
|
|
90
|
+
db: Database,
|
|
91
|
+
key: HubSettingKey,
|
|
92
|
+
value: string,
|
|
93
|
+
now: () => Date = () => new Date(),
|
|
94
|
+
): void {
|
|
95
|
+
const ts = now().toISOString();
|
|
96
|
+
db.prepare(
|
|
97
|
+
`INSERT INTO hub_settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
98
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
99
|
+
).run(key, value, ts);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove a setting. Idempotent — deleting an already-absent key is a
|
|
104
|
+
* no-op (the `auto_approve_until` consume-and-clear path relies on this
|
|
105
|
+
* shape so the OAuth register handler doesn't have to check existence
|
|
106
|
+
* twice).
|
|
107
|
+
*/
|
|
108
|
+
export function deleteSetting(db: Database, key: HubSettingKey): void {
|
|
109
|
+
db.prepare("DELETE FROM hub_settings WHERE key = ?").run(key);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- domain helpers: auto-approve window ---------------------------------
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Default window during which the first OAuth client registration is
|
|
116
|
+
* auto-approved after the wizard completes. The brief specifies 60
|
|
117
|
+
* minutes; exported as a constant so tests can clamp the clock without
|
|
118
|
+
* threading the magic number through every callsite.
|
|
119
|
+
*/
|
|
120
|
+
export const FIRST_CLIENT_AUTO_APPROVE_WINDOW_MS = 60 * 60 * 1000;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Open the auto-approve window. Called from the wizard's vault POST
|
|
124
|
+
* success path (the "wizard is now done" transition). Idempotent — if a
|
|
125
|
+
* prior window already exists, it's overwritten with a fresh expiry.
|
|
126
|
+
* That's fine: re-firing the wizard's done transition for any reason
|
|
127
|
+
* resets the window, which is the predictable behavior.
|
|
128
|
+
*/
|
|
129
|
+
export function openFirstClientAutoApproveWindow(
|
|
130
|
+
db: Database,
|
|
131
|
+
now: () => Date = () => new Date(),
|
|
132
|
+
): void {
|
|
133
|
+
const expires = new Date(now().getTime() + FIRST_CLIENT_AUTO_APPROVE_WINDOW_MS);
|
|
134
|
+
setSetting(db, "pending_first_client_auto_approve_until", expires.toISOString(), now);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check whether a first-client auto-approve window is currently open
|
|
139
|
+
* (set and in the future). Pure read; no consumption. Used by the
|
|
140
|
+
* OAuth register handler to decide whether to mint `approved` vs
|
|
141
|
+
* `pending`.
|
|
142
|
+
*/
|
|
143
|
+
export function isFirstClientAutoApproveWindowOpen(
|
|
144
|
+
db: Database,
|
|
145
|
+
now: () => Date = () => new Date(),
|
|
146
|
+
): boolean {
|
|
147
|
+
const raw = getSetting(db, "pending_first_client_auto_approve_until");
|
|
148
|
+
if (!raw) return false;
|
|
149
|
+
const expiresAt = Date.parse(raw);
|
|
150
|
+
if (Number.isNaN(expiresAt)) return false;
|
|
151
|
+
return expiresAt > now().getTime();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Consume the auto-approve window. Returns true if a window was open
|
|
156
|
+
* and was successfully consumed; false otherwise (already expired,
|
|
157
|
+
* never opened, or already consumed). The window is single-use — clear
|
|
158
|
+
* the setting on consume so the next client falls through to the
|
|
159
|
+
* standard pending-approval flow.
|
|
160
|
+
*
|
|
161
|
+
* This is the canonical entry point on the OAuth register path. The
|
|
162
|
+
* shape is "check + consume" in one call to keep the OAuth handler
|
|
163
|
+
* narrow + race-free under a single-writer assumption (hub is a single
|
|
164
|
+
* SQLite writer).
|
|
165
|
+
*/
|
|
166
|
+
export function consumeFirstClientAutoApproveWindow(
|
|
167
|
+
db: Database,
|
|
168
|
+
now: () => Date = () => new Date(),
|
|
169
|
+
): boolean {
|
|
170
|
+
if (!isFirstClientAutoApproveWindowOpen(db, now)) return false;
|
|
171
|
+
deleteSetting(db, "pending_first_client_auto_approve_until");
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- domain helpers: module install channel ------------------------------
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Which dist-tag `bun add -g <pkg>@<channel>` should use. `"latest"`
|
|
179
|
+
* tracks the stable channel; `"rc"` tracks pre-release builds. Exposed
|
|
180
|
+
* here (not buried in api-modules-ops) because the admin SPA reads it
|
|
181
|
+
* via `/api/modules` and writes it via `/api/modules/channel` — the
|
|
182
|
+
* setting is the cross-cutting source of truth, the install path is
|
|
183
|
+
* one consumer.
|
|
184
|
+
*/
|
|
185
|
+
export type ModuleInstallChannel = "latest" | "rc";
|
|
186
|
+
|
|
187
|
+
/** Exported so the API handler + the SPA toggle can share validation. */
|
|
188
|
+
export const MODULE_INSTALL_CHANNELS: readonly ModuleInstallChannel[] = ["latest", "rc"];
|
|
189
|
+
|
|
190
|
+
export function isModuleInstallChannel(s: unknown): s is ModuleInstallChannel {
|
|
191
|
+
return typeof s === "string" && MODULE_INSTALL_CHANNELS.includes(s as ModuleInstallChannel);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Env var that seeds `module_install_channel` on first read. Read only
|
|
196
|
+
* when the hub_settings row is absent — once the row exists, the env
|
|
197
|
+
* var is ignored on subsequent boots (admin must use the SPA toggle or
|
|
198
|
+
* the API to change channel). Lets Aaron's fresh-machine deploys ship
|
|
199
|
+
* with `PARACHUTE_MODULE_CHANNEL=rc` baked into the platform's env
|
|
200
|
+
* config without baking the channel into the binary or first-boot.
|
|
201
|
+
*/
|
|
202
|
+
export const PARACHUTE_MODULE_CHANNEL_ENV = "PARACHUTE_MODULE_CHANNEL";
|
|
203
|
+
|
|
204
|
+
/** Fallback when nothing else is set — the stable channel. */
|
|
205
|
+
export const DEFAULT_MODULE_INSTALL_CHANNEL: ModuleInstallChannel = "latest";
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read the configured module install channel. On first call (no row in
|
|
209
|
+
* hub_settings), seeds from `process.env.PARACHUTE_MODULE_CHANNEL` if
|
|
210
|
+
* valid, otherwise defaults to `"latest"` (an invalid env value warns
|
|
211
|
+
* + still falls back to "latest"). After that first seed, the
|
|
212
|
+
* hub_settings row is source of truth.
|
|
213
|
+
*
|
|
214
|
+
* The `env` + `warn` knobs are test seams — production uses
|
|
215
|
+
* `process.env` + `console.warn`. Tests inject a deterministic shape so
|
|
216
|
+
* the warn-on-invalid branch can be asserted without console-capture.
|
|
217
|
+
*/
|
|
218
|
+
export function getModuleInstallChannel(
|
|
219
|
+
db: Database,
|
|
220
|
+
opts: {
|
|
221
|
+
env?: NodeJS.ProcessEnv;
|
|
222
|
+
warn?: (msg: string) => void;
|
|
223
|
+
} = {},
|
|
224
|
+
): ModuleInstallChannel {
|
|
225
|
+
const existing = getSetting(db, "module_install_channel");
|
|
226
|
+
if (existing !== undefined) {
|
|
227
|
+
// Row already seeded — trust it. If somehow corrupted (manual sqlite
|
|
228
|
+
// edit, schema drift), fall back to "latest" silently rather than
|
|
229
|
+
// crashing the install path. Re-seeding the row is left to the
|
|
230
|
+
// admin's explicit setModuleInstallChannel call.
|
|
231
|
+
if (isModuleInstallChannel(existing)) return existing;
|
|
232
|
+
return DEFAULT_MODULE_INSTALL_CHANNEL;
|
|
233
|
+
}
|
|
234
|
+
const env = opts.env ?? process.env;
|
|
235
|
+
const warn = opts.warn ?? ((msg: string) => console.warn(msg));
|
|
236
|
+
const fromEnv = env[PARACHUTE_MODULE_CHANNEL_ENV];
|
|
237
|
+
let seed: ModuleInstallChannel = DEFAULT_MODULE_INSTALL_CHANNEL;
|
|
238
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
239
|
+
if (isModuleInstallChannel(fromEnv)) {
|
|
240
|
+
seed = fromEnv;
|
|
241
|
+
} else {
|
|
242
|
+
warn(
|
|
243
|
+
`[hub-settings] ${PARACHUTE_MODULE_CHANNEL_ENV}="${fromEnv}" is not a valid channel — expected one of ${MODULE_INSTALL_CHANNELS.join(", ")}. Falling back to "${DEFAULT_MODULE_INSTALL_CHANNEL}".`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
setSetting(db, "module_install_channel", seed);
|
|
248
|
+
return seed;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Write the module install channel. Validated by the caller (the API
|
|
253
|
+
* handler + the SPA already constrain to the union); the function
|
|
254
|
+
* itself only accepts the typed shape, so a TypeScript-clean callsite
|
|
255
|
+
* can't write a malformed value.
|
|
256
|
+
*/
|
|
257
|
+
export function setModuleInstallChannel(db: Database, channel: ModuleInstallChannel): void {
|
|
258
|
+
setSetting(db, "module_install_channel", channel);
|
|
259
|
+
}
|
package/src/hub.ts
CHANGED
|
@@ -487,16 +487,41 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
487
487
|
// even if the well-known fetch is slow or fails.
|
|
488
488
|
renderAdmin();
|
|
489
489
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
490
|
+
// Fetch services and render. cache 'no-store' on the fetch matters
|
|
491
|
+
// here: without it, the browser's HTTP cache returns the stale
|
|
492
|
+
// services list the next time the operator clicks back to / after
|
|
493
|
+
// installing a module via /admin/modules. Server-side also sets
|
|
494
|
+
// cache-control no-store on the well-known doc; belt-and-suspenders
|
|
495
|
+
// since older browsers (and some intermediaries) ignore one or the
|
|
496
|
+
// other (hub#268 Item 1).
|
|
497
|
+
async function loadServices() {
|
|
498
|
+
try {
|
|
499
|
+
const wk = await fetch('/.well-known/parachute.json', {
|
|
500
|
+
credentials: 'omit',
|
|
501
|
+
cache: 'no-store',
|
|
502
|
+
});
|
|
503
|
+
if (!wk.ok) throw new Error('well-known fetch failed: ' + wk.status);
|
|
504
|
+
const doc = await wk.json();
|
|
505
|
+
const services = Array.isArray(doc.services) ? doc.services : [];
|
|
506
|
+
renderServices(services);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
servicesGrid.innerHTML = '<div class="error">Could not load services: ' +
|
|
509
|
+
(err && err.message ? err.message : String(err)) + '</div>';
|
|
510
|
+
}
|
|
499
511
|
}
|
|
512
|
+
|
|
513
|
+
// Re-fetch on pageshow (covers the bfcache-restore path: when an
|
|
514
|
+
// operator clicks back from /admin/modules to / the browser may
|
|
515
|
+
// restore the prior DOM without re-running the IIFE, leaving stale
|
|
516
|
+
// tiles). The event's persisted flag is the bfcache discriminator —
|
|
517
|
+
// true when the page was rehydrated from cache, false on a fresh
|
|
518
|
+
// load. On fresh load the initial loadServices() below already ran,
|
|
519
|
+
// so we only re-fetch when persisted is true.
|
|
520
|
+
window.addEventListener('pageshow', (e) => {
|
|
521
|
+
if (e.persisted) void loadServices();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
void loadServices();
|
|
500
525
|
})();
|
|
501
526
|
</script>
|
|
502
527
|
</body>
|
package/src/jwt-sign.ts
CHANGED
|
@@ -47,6 +47,20 @@ export interface SignAccessTokenOpts {
|
|
|
47
47
|
* or thread it from `OAuthDeps.issuer`.
|
|
48
48
|
*/
|
|
49
49
|
issuer: string;
|
|
50
|
+
/**
|
|
51
|
+
* Per-user vault pin — the multi-user Phase 1 (design
|
|
52
|
+
* [`2026-05-20-multi-user-phase-1.md`](https://parachute.computer/design/2026-05-20-multi-user-phase-1/))
|
|
53
|
+
* vault_scope claim. Non-empty for non-admin users (a single-element list
|
|
54
|
+
* naming their `assigned_vault`); empty `[]` for admin users (the "no
|
|
55
|
+
* per-user vault restriction" sentinel — admins can request any vault on
|
|
56
|
+
* the hub via the consent picker). Always emitted as a claim — defaults
|
|
57
|
+
* to `[]` when callers omit — so a downstream consumer (PR 5's
|
|
58
|
+
* scope-guard at vault / notes / scribe) can unambiguously read it
|
|
59
|
+
* without distinguishing "absent" from "empty." Phase 1 always has
|
|
60
|
+
* length ≤1; the list shape carries Phase 2 multi-vault forward without
|
|
61
|
+
* a wire-shape change.
|
|
62
|
+
*/
|
|
63
|
+
vaultScope?: string[];
|
|
50
64
|
/** Override the jti (defaults to random base64url(16)). Used by tests. */
|
|
51
65
|
jti?: string;
|
|
52
66
|
/**
|
|
@@ -60,7 +74,8 @@ export interface SignAccessTokenOpts {
|
|
|
60
74
|
* `pa_scope_set` (which scope-set the token was minted under) so an
|
|
61
75
|
* auto-rotation can preserve the operator's chosen narrowing across mints.
|
|
62
76
|
* Reserved claims (`scope`, `client_id`, `sub`, `iss`, `iat`, `exp`, `aud`,
|
|
63
|
-
* `jti`) are owned by this function and overwritten if
|
|
77
|
+
* `jti`, `vault_scope`) are owned by this function and overwritten if
|
|
78
|
+
* passed here.
|
|
64
79
|
*/
|
|
65
80
|
extraClaims?: Record<string, unknown>;
|
|
66
81
|
}
|
|
@@ -85,6 +100,7 @@ export async function signAccessToken(
|
|
|
85
100
|
...(opts.extraClaims ?? {}),
|
|
86
101
|
scope: opts.scopes.join(" "),
|
|
87
102
|
client_id: opts.clientId,
|
|
103
|
+
vault_scope: opts.vaultScope ?? [],
|
|
88
104
|
})
|
|
89
105
|
.setProtectedHeader({ alg: SIGNING_ALGORITHM, kid: key.kid })
|
|
90
106
|
.setSubject(opts.sub)
|