@substrat-run/dev-issuer 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -0
- package/dist/harness.d.ts +57 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +57 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/issuer.d.ts +46 -0
- package/dist/issuer.d.ts.map +1 -0
- package/dist/issuer.js +351 -0
- package/dist/issuer.js.map +1 -0
- package/dist/keys.d.ts +37 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +37 -0
- package/dist/keys.js.map +1 -0
- package/dist/personas.d.ts +24 -0
- package/dist/personas.d.ts.map +1 -0
- package/dist/personas.js +2 -0
- package/dist/personas.js.map +1 -0
- package/package.json +49 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run the dev issuer as its own process — the entrypoint a vertical's `dev` script starts
|
|
4
|
+
* alongside its API and web servers.
|
|
5
|
+
*
|
|
6
|
+
* tsx src/cli.ts --personas ../../demos/callout/src/personas.ts --port 8879
|
|
7
|
+
*
|
|
8
|
+
* `--personas` names a module exporting `PERSONAS: DevPersona[]`. Pointing it at the
|
|
9
|
+
* vertical's own file is deliberate: the issuer's cast and the identity links the vertical
|
|
10
|
+
* seeds are then the same array, and cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
import { serve } from '@hono/node-server';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
import { createDevIssuer } from './issuer.js';
|
|
16
|
+
function arg(name) {
|
|
17
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
18
|
+
if (i !== -1 && process.argv[i + 1])
|
|
19
|
+
return process.argv[i + 1];
|
|
20
|
+
const inline = process.argv.find((a) => a.startsWith(`--${name}=`));
|
|
21
|
+
return inline?.slice(name.length + 3);
|
|
22
|
+
}
|
|
23
|
+
const personasArg = arg('personas');
|
|
24
|
+
if (!personasArg) {
|
|
25
|
+
console.error('substrat-dev-issuer: --personas <module exporting PERSONAS> is required');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const modUrl = pathToFileURL(resolve(process.cwd(), personasArg)).href;
|
|
29
|
+
const mod = (await import(modUrl));
|
|
30
|
+
const personas = mod.PERSONAS ?? mod.default;
|
|
31
|
+
if (!Array.isArray(personas) || personas.length === 0) {
|
|
32
|
+
console.error(`substrat-dev-issuer: ${personasArg} exports no non-empty PERSONAS array`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const port = Number(arg('port') ?? process.env.ISSUER_PORT ?? 8879);
|
|
36
|
+
serve({ fetch: createDevIssuer({ personas }).fetch, port });
|
|
37
|
+
console.log([
|
|
38
|
+
'',
|
|
39
|
+
' substrat · dev issuer — OIDC, no passwords',
|
|
40
|
+
' ' + '─'.repeat(52),
|
|
41
|
+
` issuer http://localhost:${port}`,
|
|
42
|
+
` discovery http://localhost:${port}/.well-known/openid-configuration`,
|
|
43
|
+
' ' + '─'.repeat(52),
|
|
44
|
+
` cast ${personas.map((p) => p.sub).join(', ')}`,
|
|
45
|
+
' note dev only — the signing key is checked in and public',
|
|
46
|
+
'',
|
|
47
|
+
].join('\n'));
|
|
48
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,SAAS,GAAG,CAAC,IAAY;IACvB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC;IACpE,OAAO,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,WAAW,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;AACpC,IAAI,CAAC,WAAW,EAAE,CAAC;IACjB,OAAO,CAAC,KAAK,CAAC,yEAAyE,CAAC,CAAC;IACzF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AACvE,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,CAAwD,CAAC;AAC1F,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC;AAC7C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACtD,OAAO,CAAC,KAAK,CAAC,wBAAwB,WAAW,sCAAsC,CAAC,CAAC;IACzF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC;AACpE,KAAK,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAE5D,OAAO,CAAC,GAAG,CACT;IACE,EAAE;IACF,8CAA8C;IAC9C,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;IACrB,gDAAgD,IAAI,EAAE;IACtD,gDAAgD,IAAI,mCAAmC;IACvF,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;IACrB,cAAc,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;IACrD,gEAAgE;IAChE,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AuthSubject } from '@substrat-run/vertical-auth/provider';
|
|
2
|
+
import type { PlatformActorId, PrincipalId, ScopeId, TenantId } from '@substrat-run/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The slice of `HostAdmin` this needs — structural, so a dev server hands in `host.admin`
|
|
5
|
+
* and nothing here depends on which adapter is underneath.
|
|
6
|
+
*/
|
|
7
|
+
export interface DevDirectory {
|
|
8
|
+
listIdentityTenants(actor: PlatformActorId, provider: string, externalId: string): Promise<TenantId[]>;
|
|
9
|
+
resolveIdentity(tenantId: TenantId, provider: string, externalId: string): Promise<{
|
|
10
|
+
principal: PrincipalId;
|
|
11
|
+
scopeId: ScopeId | null;
|
|
12
|
+
} | undefined>;
|
|
13
|
+
}
|
|
14
|
+
export interface DevLoginOptions {
|
|
15
|
+
/** Usually `host.admin`. */
|
|
16
|
+
directory: DevDirectory;
|
|
17
|
+
/** The local platform actor the directory reads are stamped with. */
|
|
18
|
+
actor: PlatformActorId;
|
|
19
|
+
/** The identity pool the seed linked these subjects into (e.g. `oidc:dev-issuer`). */
|
|
20
|
+
provider: string;
|
|
21
|
+
/** The issuer origin. Defaults to `OIDC_ISSUER`, else the dev issuer's default port. */
|
|
22
|
+
issuer?: string;
|
|
23
|
+
clientId?: string;
|
|
24
|
+
clientSecret?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Signs the local session cookie. Fixed by default so a reload does not sign you out —
|
|
27
|
+
* it protects a localhost cookie, and being logged out on every file save buys nothing.
|
|
28
|
+
*/
|
|
29
|
+
sessionSecret?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DevCaller {
|
|
32
|
+
principal: PrincipalId;
|
|
33
|
+
tenantId: TenantId;
|
|
34
|
+
scopeId: ScopeId;
|
|
35
|
+
/** The issuer's `sub` — what the directory was keyed on. */
|
|
36
|
+
sub: string;
|
|
37
|
+
/** `name`, else `email`, else the `sub`. Never empty. */
|
|
38
|
+
display: string;
|
|
39
|
+
}
|
|
40
|
+
export interface DevLogin {
|
|
41
|
+
/** Mount on `/api/auth/*`: login → issuer → callback → session cookie → logout. */
|
|
42
|
+
handle(request: Request): Promise<Response>;
|
|
43
|
+
/**
|
|
44
|
+
* The verified subject behind this request — the issuer's answer, BEFORE any directory
|
|
45
|
+
* lookup. Exposed for verticals whose tenant is not the directory's to choose: RallyPoint
|
|
46
|
+
* resolves a caller per venue, so it knows which tenant to ask about and `caller()`'s
|
|
47
|
+
* "which tenant is this login in" would be the wrong question.
|
|
48
|
+
*/
|
|
49
|
+
subject(headers: Headers): Promise<AuthSubject | null>;
|
|
50
|
+
/** The caller behind this request, or null when nobody is signed in. */
|
|
51
|
+
caller(headers: Headers): Promise<DevCaller | null>;
|
|
52
|
+
/** The issuer origin in use — for the boot banner. */
|
|
53
|
+
issuer: string;
|
|
54
|
+
}
|
|
55
|
+
export declare const DEV_ISSUER_DEFAULT = "http://localhost:8879";
|
|
56
|
+
export declare function devLogin(opts: DevLoginOptions): DevLogin;
|
|
57
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AACxE,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,OAAO,EACP,QAAQ,EACT,MAAM,yBAAyB,CAAC;AAEjC;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,mBAAmB,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACvG,eAAe,CACb,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,SAAS,EAAE,WAAW,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;CAC7E;AAED,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,SAAS,EAAE,YAAY,CAAC;IACxB,qEAAqE;IACrE,KAAK,EAAE,eAAe,CAAC;IACvB,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,CAAC;IACvB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,mFAAmF;IACnF,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACvD,wEAAwE;IACxE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACpD,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CAkCxD"}
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relying-party half, for a vertical's local dev server.
|
|
3
|
+
*
|
|
4
|
+
* Every demo's node entrypoint needs the same two steps, and they are the two steps a
|
|
5
|
+
* hosted deployment performs: verify the request against the issuer (session cookie, or a
|
|
6
|
+
* bearer for a script) to get a `sub`, then ask the identity directory which principal that
|
|
7
|
+
* subject is and where they live. Written once here rather than five times, for the reason
|
|
8
|
+
* `@substrat-run/oidc-rp` exists — this is security-relevant glue, and five copies drift.
|
|
9
|
+
*
|
|
10
|
+
* What it deliberately does NOT do is decide what an unauthenticated caller means. A demo
|
|
11
|
+
* answers 401; another checks whether its owner seat is unclaimed and says `needs-setup`
|
|
12
|
+
* instead. That is the vertical's policy, so `caller()` simply returns null.
|
|
13
|
+
*
|
|
14
|
+
* The pairing with `createDevIssuer` is only a default. Point `issuer` at anything that
|
|
15
|
+
* speaks OIDC and this is an ordinary relying party — which is the property the whole
|
|
16
|
+
* package exists to preserve.
|
|
17
|
+
*/
|
|
18
|
+
import { oidcRpAuthProvider } from '@substrat-run/vertical-auth/oidc-rp-provider';
|
|
19
|
+
export const DEV_ISSUER_DEFAULT = 'http://localhost:8879';
|
|
20
|
+
export function devLogin(opts) {
|
|
21
|
+
const issuer = opts.issuer ?? process.env.OIDC_ISSUER ?? DEV_ISSUER_DEFAULT;
|
|
22
|
+
const auth = oidcRpAuthProvider({
|
|
23
|
+
issuer,
|
|
24
|
+
// The dev issuer registers no clients and checks no secret; a hosted instance is
|
|
25
|
+
// handed real ones through `substrat:auth`. Overridable so this same harness can be
|
|
26
|
+
// pointed at an issuer that does care.
|
|
27
|
+
clientId: opts.clientId ?? process.env.OIDC_CLIENT_ID ?? 'substrat-dev',
|
|
28
|
+
clientSecret: opts.clientSecret ?? process.env.OIDC_CLIENT_SECRET ?? 'dev-issuer-checks-no-secret',
|
|
29
|
+
sessionSecret: opts.sessionSecret ?? process.env.SESSION_SECRET ?? 'substrat-local-dev-session-secret',
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
issuer,
|
|
33
|
+
handle: (request) => auth.handle(request),
|
|
34
|
+
subject: (headers) => auth.resolve(headers),
|
|
35
|
+
async caller(headers) {
|
|
36
|
+
const subject = await auth.resolve(headers);
|
|
37
|
+
if (!subject)
|
|
38
|
+
return null;
|
|
39
|
+
// Which tenant this login exists in. A central pool, so this is a legitimate
|
|
40
|
+
// question; a persona is linked in exactly one, and that link carries their scope.
|
|
41
|
+
const [tenantId] = await opts.directory.listIdentityTenants(opts.actor, opts.provider, subject.sub);
|
|
42
|
+
if (!tenantId)
|
|
43
|
+
return null;
|
|
44
|
+
const identity = await opts.directory.resolveIdentity(tenantId, opts.provider, subject.sub);
|
|
45
|
+
if (!identity?.scopeId)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
principal: identity.principal,
|
|
49
|
+
tenantId,
|
|
50
|
+
scopeId: identity.scopeId,
|
|
51
|
+
sub: subject.sub,
|
|
52
|
+
display: subject.name ?? subject.email ?? subject.sub,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AAkElF,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC;AAE1D,MAAM,UAAU,QAAQ,CAAC,IAAqB;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC5E,MAAM,IAAI,GAAG,kBAAkB,CAAC;QAC9B,MAAM;QACN,iFAAiF;QACjF,oFAAoF;QACpF,uCAAuC;QACvC,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,cAAc;QACvE,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,6BAA6B;QAClG,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,mCAAmC;KACvG,CAAC,CAAC;IAEH,OAAO;QACL,MAAM;QACN,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QACzC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QAC3C,KAAK,CAAC,MAAM,CAAC,OAAO;YAClB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAC5C,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAC1B,6EAA6E;YAC7E,mFAAmF;YACnF,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;YACpG,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC3B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;YAC5F,IAAI,CAAC,QAAQ,EAAE,OAAO;gBAAE,OAAO,IAAI,CAAC;YACpC,OAAO;gBACL,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,QAAQ;gBACR,OAAO,EAAE,QAAQ,CAAC,OAAO;gBACzB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,OAAO,EAAE,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG;aACtD,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createDevIssuer, type DevIssuerOptions } from './issuer.js';
|
|
2
|
+
export type { DevPersona } from './personas.js';
|
|
3
|
+
export { DEV_ALG, DEV_KID, DEV_PUBLIC_JWK } from './keys.js';
|
|
4
|
+
export { devLogin, DEV_ISSUER_DEFAULT, type DevCaller, type DevDirectory, type DevLogin, type DevLoginOptions, } from './harness.js';
|
|
5
|
+
export type { AuthSubject } from '@substrat-run/vertical-auth/provider';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAyB,MAAM,aAAa,CAAC;AAErE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,QAAQ,EACR,kBAAkB,GAKnB,MAAM,cAAc,CAAC"}
|
package/dist/issuer.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real OpenID Connect provider, with exactly one shortcut: `/authorize` shows a list of
|
|
3
|
+
* names and returns as soon as you click one, instead of asking for a password.
|
|
4
|
+
*
|
|
5
|
+
* Everything else is the genuine protocol — discovery, JWKS, Authorization Code + PKCE, a
|
|
6
|
+
* signed ID token with `nonce` — because the point is that the app talking to it runs its
|
|
7
|
+
* PRODUCTION login path. A vertical wired to this issuer holds no dev branch, no
|
|
8
|
+
* impersonation header and no persona list: it is an ordinary relying party, and swapping
|
|
9
|
+
* this issuer for a real one is a config change. That is the whole design.
|
|
10
|
+
*
|
|
11
|
+
* **Stateless.** There is no session store, no code store, and no issuer cookie. An
|
|
12
|
+
* authorization code IS a short-lived JWT carrying everything `/token` must check
|
|
13
|
+
* (subject, client, redirect URI, nonce, PKCE challenge), signed with the key in
|
|
14
|
+
* `keys.ts`. Two consequences worth knowing:
|
|
15
|
+
*
|
|
16
|
+
* - Restarting the issuer invalidates nothing, so a dev server and a test script can
|
|
17
|
+
* restart independently of each other.
|
|
18
|
+
* - Having no SSO session, the picker appears on EVERY `/authorize` — which is what you
|
|
19
|
+
* want from a dev issuer. Switching user is one click, not a logout dance, and
|
|
20
|
+
* `prompt=select_account` (what the platform's RP sends for "use another account")
|
|
21
|
+
* needs no special handling because it is already the only behaviour.
|
|
22
|
+
*
|
|
23
|
+
* Never deploy this. The signing key is checked in — see `keys.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { Hono } from 'hono';
|
|
26
|
+
import type { DevPersona } from './personas.js';
|
|
27
|
+
export interface DevIssuerOptions {
|
|
28
|
+
/** The cast the picker offers. The `sub` values are the contract with the app's directory. */
|
|
29
|
+
personas: DevPersona[];
|
|
30
|
+
/**
|
|
31
|
+
* Pin the `issuer` value instead of deriving it from the request's own origin.
|
|
32
|
+
* Leave unset: derived means discovery is always self-consistent with whatever host
|
|
33
|
+
* the relying party actually reached, which is what a local RP needs.
|
|
34
|
+
*/
|
|
35
|
+
issuer?: string;
|
|
36
|
+
/** ID/access token lifetime in seconds (default 1 hour). */
|
|
37
|
+
tokenTtlSec?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Extra redirect-URI prefixes to accept beyond loopback. Loopback (`http://localhost:*`,
|
|
40
|
+
* `http://127.0.0.1:*`, `http://[::1]:*`) is always accepted; anything else is refused so a
|
|
41
|
+
* stray instance cannot be pointed at a third-party URL and used as an open redirector.
|
|
42
|
+
*/
|
|
43
|
+
allowedRedirectPrefixes?: string[];
|
|
44
|
+
}
|
|
45
|
+
export declare function createDevIssuer(opts: DevIssuerOptions): Hono;
|
|
46
|
+
//# sourceMappingURL=issuer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../src/issuer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;CACpC;AA+ED,wBAAgB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CA8M5D"}
|
package/dist/issuer.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real OpenID Connect provider, with exactly one shortcut: `/authorize` shows a list of
|
|
3
|
+
* names and returns as soon as you click one, instead of asking for a password.
|
|
4
|
+
*
|
|
5
|
+
* Everything else is the genuine protocol — discovery, JWKS, Authorization Code + PKCE, a
|
|
6
|
+
* signed ID token with `nonce` — because the point is that the app talking to it runs its
|
|
7
|
+
* PRODUCTION login path. A vertical wired to this issuer holds no dev branch, no
|
|
8
|
+
* impersonation header and no persona list: it is an ordinary relying party, and swapping
|
|
9
|
+
* this issuer for a real one is a config change. That is the whole design.
|
|
10
|
+
*
|
|
11
|
+
* **Stateless.** There is no session store, no code store, and no issuer cookie. An
|
|
12
|
+
* authorization code IS a short-lived JWT carrying everything `/token` must check
|
|
13
|
+
* (subject, client, redirect URI, nonce, PKCE challenge), signed with the key in
|
|
14
|
+
* `keys.ts`. Two consequences worth knowing:
|
|
15
|
+
*
|
|
16
|
+
* - Restarting the issuer invalidates nothing, so a dev server and a test script can
|
|
17
|
+
* restart independently of each other.
|
|
18
|
+
* - Having no SSO session, the picker appears on EVERY `/authorize` — which is what you
|
|
19
|
+
* want from a dev issuer. Switching user is one click, not a logout dance, and
|
|
20
|
+
* `prompt=select_account` (what the platform's RP sends for "use another account")
|
|
21
|
+
* needs no special handling because it is already the only behaviour.
|
|
22
|
+
*
|
|
23
|
+
* Never deploy this. The signing key is checked in — see `keys.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { Hono } from 'hono';
|
|
26
|
+
import { SignJWT, jwtVerify, importJWK } from 'jose';
|
|
27
|
+
import { DEV_ALG, DEV_KID, DEV_PRIVATE_JWK, DEV_PUBLIC_JWK } from './keys.js';
|
|
28
|
+
const CODE_TTL_SEC = 300;
|
|
29
|
+
let cachedPrivate;
|
|
30
|
+
let cachedPublic;
|
|
31
|
+
function signingKey() {
|
|
32
|
+
cachedPrivate ??= importJWK(DEV_PRIVATE_JWK, DEV_ALG);
|
|
33
|
+
return cachedPrivate;
|
|
34
|
+
}
|
|
35
|
+
function verificationKey() {
|
|
36
|
+
cachedPublic ??= importJWK(DEV_PUBLIC_JWK, DEV_ALG);
|
|
37
|
+
return cachedPublic;
|
|
38
|
+
}
|
|
39
|
+
const enc = new TextEncoder();
|
|
40
|
+
const b64url = (bytes) => {
|
|
41
|
+
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
42
|
+
let s = '';
|
|
43
|
+
for (const byte of b)
|
|
44
|
+
s += String.fromCharCode(byte);
|
|
45
|
+
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
46
|
+
};
|
|
47
|
+
async function sign(claims, ttlSec) {
|
|
48
|
+
return new SignJWT(claims)
|
|
49
|
+
.setProtectedHeader({ alg: DEV_ALG, kid: DEV_KID, typ: 'JWT' })
|
|
50
|
+
.setIssuedAt()
|
|
51
|
+
.setExpirationTime(`${ttlSec}s`)
|
|
52
|
+
.sign(await signingKey());
|
|
53
|
+
}
|
|
54
|
+
/** Loopback always; anything else only if explicitly allowed. */
|
|
55
|
+
function redirectAllowed(uri, extra) {
|
|
56
|
+
let u;
|
|
57
|
+
try {
|
|
58
|
+
u = new URL(uri);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const loopback = u.protocol === 'http:' && (u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]' || u.hostname === '::1');
|
|
64
|
+
return loopback || extra.some((p) => uri.startsWith(p));
|
|
65
|
+
}
|
|
66
|
+
const escapeHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
67
|
+
/**
|
|
68
|
+
* Send the browser back to the relying party with an OAuth error, per RFC 6749 §4.1.2.1 —
|
|
69
|
+
* but only once the redirect URI is known to be safe. Before that point the only correct
|
|
70
|
+
* answer is a plain response, because redirecting is exactly what we have not yet validated.
|
|
71
|
+
*/
|
|
72
|
+
function redirectError(c, redirectUri, state, error, description) {
|
|
73
|
+
const u = new URL(redirectUri);
|
|
74
|
+
u.searchParams.set('error', error);
|
|
75
|
+
u.searchParams.set('error_description', description);
|
|
76
|
+
if (state)
|
|
77
|
+
u.searchParams.set('state', state);
|
|
78
|
+
return c.redirect(u.toString());
|
|
79
|
+
}
|
|
80
|
+
export function createDevIssuer(opts) {
|
|
81
|
+
const personas = opts.personas;
|
|
82
|
+
const ttl = opts.tokenTtlSec ?? 3600;
|
|
83
|
+
const extraRedirects = opts.allowedRedirectPrefixes ?? [];
|
|
84
|
+
const app = new Hono();
|
|
85
|
+
const issuerOf = (c) => opts.issuer ?? new URL(c.req.url).origin;
|
|
86
|
+
const personaOf = (sub) => personas.find((p) => p.sub === sub);
|
|
87
|
+
const claimsFor = (p) => ({
|
|
88
|
+
sub: p.sub,
|
|
89
|
+
name: p.name,
|
|
90
|
+
email: p.email,
|
|
91
|
+
email_verified: true,
|
|
92
|
+
});
|
|
93
|
+
// ── Discovery ──────────────────────────────────────────────────────────────────────
|
|
94
|
+
app.get('/.well-known/openid-configuration', (c) => {
|
|
95
|
+
const issuer = issuerOf(c);
|
|
96
|
+
return c.json({
|
|
97
|
+
issuer,
|
|
98
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
99
|
+
token_endpoint: `${issuer}/token`,
|
|
100
|
+
jwks_uri: `${issuer}/jwks.json`,
|
|
101
|
+
userinfo_endpoint: `${issuer}/userinfo`,
|
|
102
|
+
end_session_endpoint: `${issuer}/logout`,
|
|
103
|
+
response_types_supported: ['code'],
|
|
104
|
+
grant_types_supported: ['authorization_code'],
|
|
105
|
+
subject_types_supported: ['public'],
|
|
106
|
+
id_token_signing_alg_values_supported: [DEV_ALG],
|
|
107
|
+
scopes_supported: ['openid', 'email', 'profile'],
|
|
108
|
+
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
|
109
|
+
code_challenge_methods_supported: ['S256'],
|
|
110
|
+
claims_supported: ['sub', 'name', 'email', 'email_verified', 'nonce', 'aud', 'exp', 'iat', 'iss'],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
app.get('/jwks.json', (c) => c.json({ keys: [DEV_PUBLIC_JWK] }));
|
|
114
|
+
// ── Authorization ──────────────────────────────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* One endpoint, two states: without `sub` it renders the picker, with a known `sub` it
|
|
117
|
+
* issues the code. The picker's links carry the original query string back verbatim plus
|
|
118
|
+
* the chosen `sub`, so nothing about the request needs to be remembered between them —
|
|
119
|
+
* which is what lets the issuer keep no state at all.
|
|
120
|
+
*/
|
|
121
|
+
app.get('/authorize', async (c) => {
|
|
122
|
+
const q = c.req.query();
|
|
123
|
+
const redirectUri = q.redirect_uri ?? '';
|
|
124
|
+
// Validate the redirect target FIRST: every other error is reported by redirecting to
|
|
125
|
+
// it, and an unvalidated redirect is the one thing that must never happen.
|
|
126
|
+
if (!redirectUri)
|
|
127
|
+
return c.text('missing redirect_uri', 400);
|
|
128
|
+
if (!redirectAllowed(redirectUri, extraRedirects)) {
|
|
129
|
+
return c.text(`refusing to redirect to ${redirectUri} — the dev issuer accepts loopback URIs only ` +
|
|
130
|
+
'(pass allowedRedirectPrefixes to widen it)', 400);
|
|
131
|
+
}
|
|
132
|
+
const req = {
|
|
133
|
+
clientId: q.client_id ?? '',
|
|
134
|
+
redirectUri,
|
|
135
|
+
state: q.state,
|
|
136
|
+
nonce: q.nonce,
|
|
137
|
+
codeChallenge: q.code_challenge,
|
|
138
|
+
scope: q.scope ?? 'openid',
|
|
139
|
+
prompt: q.prompt,
|
|
140
|
+
loginHint: q.login_hint,
|
|
141
|
+
};
|
|
142
|
+
if (!req.clientId)
|
|
143
|
+
return redirectError(c, redirectUri, req.state, 'invalid_request', 'missing client_id');
|
|
144
|
+
if ((q.response_type ?? '') !== 'code') {
|
|
145
|
+
return redirectError(c, redirectUri, req.state, 'unsupported_response_type', 'only response_type=code is supported');
|
|
146
|
+
}
|
|
147
|
+
if (q.code_challenge && (q.code_challenge_method ?? 'plain') !== 'S256') {
|
|
148
|
+
return redirectError(c, redirectUri, req.state, 'invalid_request', 'only code_challenge_method=S256 is supported');
|
|
149
|
+
}
|
|
150
|
+
// No SSO session exists here by design, so `prompt=none` can only ever fail — and
|
|
151
|
+
// saying so is more useful than silently showing the picker it asked us not to.
|
|
152
|
+
if (req.prompt === 'none') {
|
|
153
|
+
return redirectError(c, redirectUri, req.state, 'login_required', 'the dev issuer holds no session; a user must be picked');
|
|
154
|
+
}
|
|
155
|
+
const sub = q.sub;
|
|
156
|
+
if (!sub)
|
|
157
|
+
return c.html(pickerPage(req, personas, issuerOf(c)));
|
|
158
|
+
const persona = personaOf(sub);
|
|
159
|
+
if (!persona)
|
|
160
|
+
return c.html(pickerPage(req, personas, issuerOf(c), `Unknown user "${sub}".`), 400);
|
|
161
|
+
const code = await sign({
|
|
162
|
+
typ: 'code',
|
|
163
|
+
sub: persona.sub,
|
|
164
|
+
cid: req.clientId,
|
|
165
|
+
ru: req.redirectUri,
|
|
166
|
+
...(req.nonce ? { n: req.nonce } : {}),
|
|
167
|
+
...(req.codeChallenge ? { cc: req.codeChallenge } : {}),
|
|
168
|
+
}, CODE_TTL_SEC);
|
|
169
|
+
const back = new URL(req.redirectUri);
|
|
170
|
+
back.searchParams.set('code', code);
|
|
171
|
+
if (req.state)
|
|
172
|
+
back.searchParams.set('state', req.state);
|
|
173
|
+
return c.redirect(back.toString());
|
|
174
|
+
});
|
|
175
|
+
// ── Token ──────────────────────────────────────────────────────────────────────────
|
|
176
|
+
/**
|
|
177
|
+
* The client secret is accepted and NOT checked. A dev issuer that registered no clients
|
|
178
|
+
* has no secret to compare against, and pretending otherwise would only mean inventing a
|
|
179
|
+
* registration step — the friction this exists to remove. PKCE *is* checked, because it
|
|
180
|
+
* is what binds the exchange to the browser that began it and the RP always sends it.
|
|
181
|
+
*/
|
|
182
|
+
app.post('/token', async (c) => {
|
|
183
|
+
const form = await c.req.parseBody();
|
|
184
|
+
const str = (k) => (typeof form[k] === 'string' ? form[k] : '');
|
|
185
|
+
const fail = (error, description, status = 400) => c.json({ error, error_description: description }, status);
|
|
186
|
+
if (str('grant_type') !== 'authorization_code') {
|
|
187
|
+
return fail('unsupported_grant_type', 'only authorization_code is supported');
|
|
188
|
+
}
|
|
189
|
+
const code = str('code');
|
|
190
|
+
if (!code)
|
|
191
|
+
return fail('invalid_request', 'missing code');
|
|
192
|
+
let payload;
|
|
193
|
+
try {
|
|
194
|
+
payload = (await jwtVerify(code, await verificationKey())).payload;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return fail('invalid_grant', 'the authorization code is invalid or has expired');
|
|
198
|
+
}
|
|
199
|
+
if (payload.typ !== 'code')
|
|
200
|
+
return fail('invalid_grant', 'not an authorization code');
|
|
201
|
+
if (payload.cid !== str('client_id'))
|
|
202
|
+
return fail('invalid_grant', 'client_id does not match the code');
|
|
203
|
+
if (payload.ru !== str('redirect_uri'))
|
|
204
|
+
return fail('invalid_grant', 'redirect_uri does not match the code');
|
|
205
|
+
if (payload.cc) {
|
|
206
|
+
const verifier = str('code_verifier');
|
|
207
|
+
if (!verifier)
|
|
208
|
+
return fail('invalid_grant', 'missing code_verifier');
|
|
209
|
+
const challenge = b64url(await crypto.subtle.digest('SHA-256', enc.encode(verifier)));
|
|
210
|
+
if (challenge !== payload.cc)
|
|
211
|
+
return fail('invalid_grant', 'PKCE verification failed');
|
|
212
|
+
}
|
|
213
|
+
const persona = personaOf(String(payload.sub));
|
|
214
|
+
if (!persona)
|
|
215
|
+
return fail('invalid_grant', 'the code names a user this issuer no longer serves');
|
|
216
|
+
const issuer = issuerOf(c);
|
|
217
|
+
const base = { iss: issuer, aud: payload.cid, ...claimsFor(persona) };
|
|
218
|
+
const [idToken, accessToken] = await Promise.all([
|
|
219
|
+
sign({ ...base, ...(payload.n ? { nonce: payload.n } : {}) }, ttl),
|
|
220
|
+
sign({ ...base, typ: 'access' }, ttl),
|
|
221
|
+
]);
|
|
222
|
+
return c.json({ token_type: 'Bearer', expires_in: ttl, id_token: idToken, access_token: accessToken, scope: 'openid email profile' });
|
|
223
|
+
});
|
|
224
|
+
app.get('/userinfo', async (c) => {
|
|
225
|
+
const auth = c.req.header('authorization') ?? '';
|
|
226
|
+
const token = auth.toLowerCase().startsWith('bearer ') ? auth.slice(7) : '';
|
|
227
|
+
if (!token)
|
|
228
|
+
return c.json({ error: 'invalid_token' }, 401);
|
|
229
|
+
try {
|
|
230
|
+
const { payload } = await jwtVerify(token, await verificationKey(), { issuer: issuerOf(c) });
|
|
231
|
+
const persona = personaOf(String(payload.sub));
|
|
232
|
+
if (!persona)
|
|
233
|
+
return c.json({ error: 'invalid_token' }, 401);
|
|
234
|
+
return c.json(claimsFor(persona));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return c.json({ error: 'invalid_token' }, 401);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
/**
|
|
241
|
+
* RP-initiated logout. There is no issuer session to end — the picker always asks — so
|
|
242
|
+
* this only honours `post_logout_redirect_uri`, which is what makes the relying party's
|
|
243
|
+
* `?federated` logout land somewhere sensible instead of erroring.
|
|
244
|
+
*/
|
|
245
|
+
app.get('/logout', (c) => {
|
|
246
|
+
const to = c.req.query('post_logout_redirect_uri');
|
|
247
|
+
if (to && redirectAllowed(to, extraRedirects))
|
|
248
|
+
return c.redirect(to);
|
|
249
|
+
return c.html(signedOutPage());
|
|
250
|
+
});
|
|
251
|
+
// ── The non-interactive door ───────────────────────────────────────────────────────
|
|
252
|
+
/**
|
|
253
|
+
* Mint tokens for a persona with no browser — the escape hatch a test, a curl, or a
|
|
254
|
+
* headless verification script needs.
|
|
255
|
+
*
|
|
256
|
+
* This IS impersonation, and putting it here is the point: it lives in a process that
|
|
257
|
+
* binds to localhost and is never deployed, rather than behind a flag in the vertical,
|
|
258
|
+
* where the same capability ships to production and only an environment variable stands
|
|
259
|
+
* between it and a cross-tenant hole.
|
|
260
|
+
*/
|
|
261
|
+
app.post('/dev/token', async (c) => {
|
|
262
|
+
const body = await c.req.json().catch(() => ({}));
|
|
263
|
+
const sub = typeof body.sub === 'string' ? body.sub : '';
|
|
264
|
+
const audience = typeof body.audience === 'string' ? body.audience : 'dev';
|
|
265
|
+
const persona = personaOf(sub);
|
|
266
|
+
if (!persona) {
|
|
267
|
+
return c.json({ error: `unknown sub '${sub}' — known: ${personas.map((p) => p.sub).join(', ')}` }, 400);
|
|
268
|
+
}
|
|
269
|
+
const base = { iss: issuerOf(c), aud: audience, ...claimsFor(persona) };
|
|
270
|
+
const [idToken, accessToken] = await Promise.all([sign(base, ttl), sign({ ...base, typ: 'access' }, ttl)]);
|
|
271
|
+
return c.json({ token_type: 'Bearer', expires_in: ttl, id_token: idToken, access_token: accessToken });
|
|
272
|
+
});
|
|
273
|
+
/** The cast as JSON — what a script enumerates before minting. */
|
|
274
|
+
app.get('/dev/personas', (c) => c.json(personas));
|
|
275
|
+
return app;
|
|
276
|
+
}
|
|
277
|
+
// ── The picker ───────────────────────────────────────────────────────────────────────
|
|
278
|
+
/**
|
|
279
|
+
* Server-rendered, no build step, no client JavaScript. Each entry is a plain link back to
|
|
280
|
+
* `/authorize` carrying the original request plus `sub`, so picking a user is one GET and
|
|
281
|
+
* the browser's own back button behaves.
|
|
282
|
+
*/
|
|
283
|
+
function pickerPage(req, personas, issuer, error) {
|
|
284
|
+
const params = new URLSearchParams({
|
|
285
|
+
response_type: 'code',
|
|
286
|
+
client_id: req.clientId,
|
|
287
|
+
redirect_uri: req.redirectUri,
|
|
288
|
+
scope: req.scope,
|
|
289
|
+
...(req.state ? { state: req.state } : {}),
|
|
290
|
+
...(req.nonce ? { nonce: req.nonce } : {}),
|
|
291
|
+
...(req.codeChallenge ? { code_challenge: req.codeChallenge, code_challenge_method: 'S256' } : {}),
|
|
292
|
+
});
|
|
293
|
+
const hinted = req.loginHint?.toLowerCase();
|
|
294
|
+
const rows = personas
|
|
295
|
+
.map((p) => {
|
|
296
|
+
const href = `/authorize?${params.toString()}&sub=${encodeURIComponent(p.sub)}`;
|
|
297
|
+
const match = hinted && p.email.toLowerCase() === hinted ? ' match' : '';
|
|
298
|
+
return `<a class="who${match}" href="${escapeHtml(href)}">
|
|
299
|
+
<span class="avatar">${escapeHtml([...p.name][0] ?? '?')}</span>
|
|
300
|
+
<span class="lines">
|
|
301
|
+
<strong>${escapeHtml(p.name)}</strong>
|
|
302
|
+
<small>${escapeHtml(p.note ?? p.email)}</small>
|
|
303
|
+
</span>
|
|
304
|
+
<code>${escapeHtml(p.sub)}</code>
|
|
305
|
+
</a>`;
|
|
306
|
+
})
|
|
307
|
+
.join('\n');
|
|
308
|
+
return page('Pick a user', `<h1>Sign in</h1>
|
|
309
|
+
<p class="sub">Development issuer at <code>${escapeHtml(issuer)}</code> — pick who to be.
|
|
310
|
+
<strong>${escapeHtml(req.clientId)}</strong> is asking.</p>
|
|
311
|
+
${error ? `<p class="error">${escapeHtml(error)}</p>` : ''}
|
|
312
|
+
<div class="list">${rows}</div>
|
|
313
|
+
<p class="foot">No passwords: this issuer authenticates nobody. It exists so the app runs its real
|
|
314
|
+
OIDC login locally. Never deploy it — its signing key is public.</p>`);
|
|
315
|
+
}
|
|
316
|
+
function signedOutPage() {
|
|
317
|
+
return page('Signed out', `<h1>Signed out</h1><p class="sub">You can close this tab.</p>`);
|
|
318
|
+
}
|
|
319
|
+
function page(title, body) {
|
|
320
|
+
return `<!doctype html>
|
|
321
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
322
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
323
|
+
<title>${escapeHtml(title)}</title>
|
|
324
|
+
<style>
|
|
325
|
+
:root { color-scheme: light dark; --bg:#f6f7f9; --card:#fff; --fg:#14181f; --muted:#5b6472; --line:#e3e6ea; --accent:#3b5bdb; }
|
|
326
|
+
@media (prefers-color-scheme: dark) {
|
|
327
|
+
:root { --bg:#14181f; --card:#1c2129; --fg:#e8ebef; --muted:#98a2b3; --line:#2b323c; --accent:#7d94f5; }
|
|
328
|
+
}
|
|
329
|
+
* { box-sizing: border-box; }
|
|
330
|
+
body { margin:0; min-height:100vh; display:grid; place-items:center; background:var(--bg); color:var(--fg);
|
|
331
|
+
font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; padding:32px 16px; }
|
|
332
|
+
.card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:28px; width:min(460px,100%); }
|
|
333
|
+
h1 { margin:0 0 6px; font-size:20px; }
|
|
334
|
+
.sub { margin:0 0 18px; color:var(--muted); font-size:13px; }
|
|
335
|
+
.error { margin:0 0 14px; padding:8px 10px; border-radius:8px; background:#fdecec; color:#a02020; font-size:13px; }
|
|
336
|
+
@media (prefers-color-scheme: dark) { .error { background:#3a1f22; color:#f5b5b5; } }
|
|
337
|
+
.list { display:flex; flex-direction:column; gap:8px; }
|
|
338
|
+
.who { display:flex; align-items:center; gap:12px; padding:10px 12px; border:1px solid var(--line); border-radius:10px;
|
|
339
|
+
text-decoration:none; color:inherit; }
|
|
340
|
+
.who:hover { border-color:var(--accent); background:color-mix(in srgb, var(--accent) 8%, transparent); }
|
|
341
|
+
.who.match { border-color:var(--accent); }
|
|
342
|
+
.avatar { flex:0 0 34px; height:34px; border-radius:50%; display:grid; place-items:center; font-weight:600;
|
|
343
|
+
background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent); }
|
|
344
|
+
.lines { display:flex; flex-direction:column; min-width:0; flex:1; }
|
|
345
|
+
.lines small { color:var(--muted); font-size:12px; }
|
|
346
|
+
code { font:12px ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--muted); }
|
|
347
|
+
.foot { margin:18px 0 0; color:var(--muted); font-size:12px; }
|
|
348
|
+
</style></head>
|
|
349
|
+
<body><div class="card">${body}</div></body></html>`;
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=issuer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuer.js","sourceRoot":"","sources":["../src/issuer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAmB,MAAM,MAAM,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAsB9E,MAAM,YAAY,GAAG,GAAG,CAAC;AAqBzB,IAAI,aAA+C,CAAC;AACpD,IAAI,YAA8C,CAAC;AACnD,SAAS,UAAU;IACjB,aAAa,KAAK,SAAS,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IACtD,OAAO,aAAa,CAAC;AACvB,CAAC;AACD,SAAS,eAAe;IACtB,YAAY,KAAK,SAAS,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACpD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;AAC9B,MAAM,MAAM,GAAG,CAAC,KAA+B,EAAU,EAAE;IACzD,MAAM,CAAC,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACtE,IAAI,CAAC,GAAG,EAAE,CAAC;IACX,KAAK,MAAM,IAAI,IAAI,CAAC;QAAE,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC5E,CAAC,CAAC;AAEF,KAAK,UAAU,IAAI,CAAC,MAAkB,EAAE,MAAc;IACpD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;SACvB,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;SAC9D,WAAW,EAAE;SACb,iBAAiB,CAAC,GAAG,MAAM,GAAG,CAAC;SAC/B,IAAI,CAAC,MAAM,UAAU,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,iEAAiE;AACjE,SAAS,eAAe,CAAC,GAAW,EAAE,KAAe;IACnD,IAAI,CAAM,CAAC;IACX,IAAI,CAAC;QACH,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,QAAQ,GACZ,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC;IACzI,OAAO,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CACvC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAE/F;;;;GAIG;AACH,SAAS,aAAa,CAAC,CAAU,EAAE,WAAmB,EAAE,KAAyB,EAAE,KAAa,EAAE,WAAmB;IACnH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IAC/B,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAC;IACrD,IAAI,KAAK;QAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAsB;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC;IACrC,MAAM,cAAc,GAAG,IAAI,CAAC,uBAAuB,IAAI,EAAE,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAU,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IAClF,MAAM,SAAS,GAAG,CAAC,GAAW,EAA0B,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;IAE/F,MAAM,SAAS,GAAG,CAAC,CAAa,EAAE,EAAE,CAAC,CAAC;QACpC,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,cAAc,EAAE,IAAI;KACrB,CAAC,CAAC;IAEH,sFAAsF;IACtF,GAAG,CAAC,GAAG,CAAC,mCAAmC,EAAE,CAAC,CAAC,EAAE,EAAE;QACjD,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,MAAM;YACN,sBAAsB,EAAE,GAAG,MAAM,YAAY;YAC7C,cAAc,EAAE,GAAG,MAAM,QAAQ;YACjC,QAAQ,EAAE,GAAG,MAAM,YAAY;YAC/B,iBAAiB,EAAE,GAAG,MAAM,WAAW;YACvC,oBAAoB,EAAE,GAAG,MAAM,SAAS;YACxC,wBAAwB,EAAE,CAAC,MAAM,CAAC;YAClC,qBAAqB,EAAE,CAAC,oBAAoB,CAAC;YAC7C,uBAAuB,EAAE,CAAC,QAAQ,CAAC;YACnC,qCAAqC,EAAE,CAAC,OAAO,CAAC;YAChD,gBAAgB,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;YAChD,qCAAqC,EAAE,CAAC,oBAAoB,EAAE,MAAM,CAAC;YACrE,gCAAgC,EAAE,CAAC,MAAM,CAAC;YAC1C,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC;SAClG,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;IAEjE,sFAAsF;IACtF;;;;;OAKG;IACH,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAChC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACxB,MAAM,WAAW,GAAG,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC;QACzC,sFAAsF;QACtF,2EAA2E;QAC3E,IAAI,CAAC,WAAW;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;QAC7D,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,cAAc,CAAC,EAAE,CAAC;YAClD,OAAO,CAAC,CAAC,IAAI,CACX,2BAA2B,WAAW,+CAA+C;gBACnF,4CAA4C,EAC9C,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAiB;YACxB,QAAQ,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;YAC3B,WAAW;YACX,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,aAAa,EAAE,CAAC,CAAC,cAAc;YAC/B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,QAAQ;YAC1B,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,SAAS,EAAE,CAAC,CAAC,UAAU;SACxB,CAAC;QAEF,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,OAAO,aAAa,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,CAAC,CAAC;QAC3G,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC;YACvC,OAAO,aAAa,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,2BAA2B,EAAE,sCAAsC,CAAC,CAAC;QACvH,CAAC;QACD,IAAI,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,qBAAqB,IAAI,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC;YACxE,OAAO,aAAa,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,iBAAiB,EAAE,8CAA8C,CAAC,CAAC;QACrH,CAAC;QACD,kFAAkF;QAClF,gFAAgF;QAChF,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1B,OAAO,aAAa,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,gBAAgB,EAAE,wDAAwD,CAAC,CAAC;QAC9H,CAAC;QAED,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QAClB,IAAI,CAAC,GAAG;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;QAEnG,MAAM,IAAI,GAAG,MAAM,IAAI,CACrB;YACE,GAAG,EAAE,MAAM;YACX,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,GAAG,EAAE,GAAG,CAAC,QAAQ;YACjB,EAAE,EAAE,GAAG,CAAC,WAAW;YACnB,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD,EACD,YAAY,CACb,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACtC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,GAAG,CAAC,KAAK;YAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,sFAAsF;IACtF;;;;;OAKG;IACH,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5F,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,WAAmB,EAAE,MAAM,GAAc,GAAG,EAAE,EAAE,CAC3E,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC;QAE5D,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,oBAAoB,EAAE,CAAC;YAC/C,OAAO,IAAI,CAAC,wBAAwB,EAAE,sCAAsC,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;QAE1D,IAAI,OAA0F,CAAC;QAC/F,IAAI,CAAC;YACH,OAAO,GAAG,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,eAAe,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,eAAe,EAAE,kDAAkD,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,eAAe,EAAE,2BAA2B,CAAC,CAAC;QACtF,IAAI,OAAO,CAAC,GAAG,KAAK,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,IAAI,CAAC,eAAe,EAAE,mCAAmC,CAAC,CAAC;QACxG,IAAI,OAAO,CAAC,EAAE,KAAK,GAAG,CAAC,cAAc,CAAC;YAAE,OAAO,IAAI,CAAC,eAAe,EAAE,sCAAsC,CAAC,CAAC;QAE7G,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,QAAQ,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC;YACtC,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAAC;YACrE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YACtF,IAAI,SAAS,KAAK,OAAO,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;QACzF,CAAC;QAED,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,eAAe,EAAE,oDAAoD,CAAC,CAAC;QAEjG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC/C,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC;YAClE,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC;SACtC,CAAC,CAAC;QACH,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAC;IACxI,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC/B,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,GAAG,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,MAAM,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC7F,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/C,IAAI,CAAC,OAAO;gBAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,GAAG,CAAC,CAAC;YAC7D,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,GAAG,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;OAIG;IACH,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QACvB,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QACnD,IAAI,EAAE,IAAI,eAAe,CAAC,EAAE,EAAE,cAAc,CAAC;YAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrE,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,sFAAsF;IACtF;;;;;;;;OAQG;IACH,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACjC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAA4B,CAAC,CAAC;QAC7E,MAAM,GAAG,GAAG,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,MAAM,QAAQ,GAAG,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;QAC3E,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,cAAc,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAC1G,CAAC;QACD,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QACxE,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3G,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,kEAAkE;IAClE,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAElD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wFAAwF;AAExF;;;;GAIG;AACH,SAAS,UAAU,CAAC,GAAiB,EAAE,QAAsB,EAAE,MAAc,EAAE,KAAc;IAC3F,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,aAAa,EAAE,MAAM;QACrB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,YAAY,EAAE,GAAG,CAAC,WAAW;QAC7B,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1C,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,aAAa,EAAE,qBAAqB,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnG,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAG,QAAQ;SAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,IAAI,GAAG,cAAc,MAAM,CAAC,QAAQ,EAAE,QAAQ,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;QAChF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,OAAO,gBAAgB,KAAK,WAAW,UAAU,CAAC,IAAI,CAAC;+BAC9B,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;;oBAE5C,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;mBACnB,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC;;gBAEhC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC;WACtB,CAAC;IACR,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO,IAAI,CACT,aAAa,EACb;kDAC8C,UAAU,CAAC,MAAM,CAAC;kBAClD,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;OACnC,KAAK,CAAC,CAAC,CAAC,oBAAoB,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;yBACtC,IAAI;;6EAEgD,CAC1E,CAAC;AACJ,CAAC;AAED,SAAS,aAAa;IACpB,OAAO,IAAI,CAAC,YAAY,EAAE,+DAA+D,CAAC,CAAC;AAC7F,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,IAAY;IACvC,OAAO;;;SAGA,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;0BA0BA,IAAI,sBAAsB,CAAC;AACrD,CAAC"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev issuer's signing key — CHECKED IN, PUBLIC, AND DELIBERATELY SO.
|
|
3
|
+
*
|
|
4
|
+
* A relying party verifies an ID token against the issuer's JWKS, so the issuer needs a
|
|
5
|
+
* keypair. Generating one at boot would work, but every restart would invalidate every
|
|
6
|
+
* session a developer holds, and the `/dev/token` bearers a test script minted would stop
|
|
7
|
+
* verifying halfway through a run. So the key is FIXED, and fixed means public: this file
|
|
8
|
+
* is in the repository, and anyone can mint a token that this issuer's JWKS will validate.
|
|
9
|
+
*
|
|
10
|
+
* That is the correct posture for a process that binds to localhost, hands out logins by
|
|
11
|
+
* clicking a name, and is never deployed. It is also the reason nothing may point a
|
|
12
|
+
* PRODUCTION relying party at a dev issuer: the trust anchor is this file.
|
|
13
|
+
*
|
|
14
|
+
* ES256 (P-256) rather than RS256 — smaller, and `createRemoteJWKSet` accepts both.
|
|
15
|
+
*/
|
|
16
|
+
/** The `kid` every token this issuer signs carries, and the only key its JWKS publishes. */
|
|
17
|
+
export declare const DEV_KID = "substrat-dev-issuer";
|
|
18
|
+
export declare const DEV_ALG = "ES256";
|
|
19
|
+
/** The private half. Public by construction — see the file header. */
|
|
20
|
+
export declare const DEV_PRIVATE_JWK: {
|
|
21
|
+
readonly kty: 'EC';
|
|
22
|
+
readonly crv: 'P-256';
|
|
23
|
+
readonly x: 'EM7WyKnjAYcG4u-ZrQV-MnDWYl7M4AKVody95UZaPw4';
|
|
24
|
+
readonly y: 'GJFEBnTwfQmMv-q1N-vv6GNMDp_i7lVeyoOaHXEt0E0';
|
|
25
|
+
readonly d: 'ut3dmM8nwUpdo-7yygjdzUyksvJ6asXBf7KFKDJ8wus';
|
|
26
|
+
};
|
|
27
|
+
/** The public half, in the shape `/jwks.json` serves it. */
|
|
28
|
+
export declare const DEV_PUBLIC_JWK: {
|
|
29
|
+
readonly kty: 'EC';
|
|
30
|
+
readonly crv: 'P-256';
|
|
31
|
+
readonly x: "EM7WyKnjAYcG4u-ZrQV-MnDWYl7M4AKVody95UZaPw4";
|
|
32
|
+
readonly y: "GJFEBnTwfQmMv-q1N-vv6GNMDp_i7lVeyoOaHXEt0E0";
|
|
33
|
+
readonly use: 'sig';
|
|
34
|
+
readonly alg: "ES256";
|
|
35
|
+
readonly kid: "substrat-dev-issuer";
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,OAAO,wBAAwB,CAAC;AAE7C,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,sEAAsE;AACtE,eAAO,MAAM,eAAe;aAC1B,GAAG,EAAE,IAAI;aACT,GAAG,EAAE,OAAO;aACZ,CAAC,EAAE,6CAA6C;aAChD,CAAC,EAAE,6CAA6C;aAChD,CAAC,EAAE,6CAA6C;CACxC,CAAC;AAEX,4DAA4D;AAC5D,eAAO,MAAM,cAAc;aACzB,GAAG,EAAE,IAAI;aACT,GAAG,EAAE,OAAO;aACZ,CAAC;aACD,CAAC;aACD,GAAG,EAAE,KAAK;aACV,GAAG;aACH,GAAG;CACK,CAAC"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev issuer's signing key — CHECKED IN, PUBLIC, AND DELIBERATELY SO.
|
|
3
|
+
*
|
|
4
|
+
* A relying party verifies an ID token against the issuer's JWKS, so the issuer needs a
|
|
5
|
+
* keypair. Generating one at boot would work, but every restart would invalidate every
|
|
6
|
+
* session a developer holds, and the `/dev/token` bearers a test script minted would stop
|
|
7
|
+
* verifying halfway through a run. So the key is FIXED, and fixed means public: this file
|
|
8
|
+
* is in the repository, and anyone can mint a token that this issuer's JWKS will validate.
|
|
9
|
+
*
|
|
10
|
+
* That is the correct posture for a process that binds to localhost, hands out logins by
|
|
11
|
+
* clicking a name, and is never deployed. It is also the reason nothing may point a
|
|
12
|
+
* PRODUCTION relying party at a dev issuer: the trust anchor is this file.
|
|
13
|
+
*
|
|
14
|
+
* ES256 (P-256) rather than RS256 — smaller, and `createRemoteJWKSet` accepts both.
|
|
15
|
+
*/
|
|
16
|
+
/** The `kid` every token this issuer signs carries, and the only key its JWKS publishes. */
|
|
17
|
+
export const DEV_KID = 'substrat-dev-issuer';
|
|
18
|
+
export const DEV_ALG = 'ES256';
|
|
19
|
+
/** The private half. Public by construction — see the file header. */
|
|
20
|
+
export const DEV_PRIVATE_JWK = {
|
|
21
|
+
kty: 'EC',
|
|
22
|
+
crv: 'P-256',
|
|
23
|
+
x: 'EM7WyKnjAYcG4u-ZrQV-MnDWYl7M4AKVody95UZaPw4',
|
|
24
|
+
y: 'GJFEBnTwfQmMv-q1N-vv6GNMDp_i7lVeyoOaHXEt0E0',
|
|
25
|
+
d: 'ut3dmM8nwUpdo-7yygjdzUyksvJ6asXBf7KFKDJ8wus',
|
|
26
|
+
};
|
|
27
|
+
/** The public half, in the shape `/jwks.json` serves it. */
|
|
28
|
+
export const DEV_PUBLIC_JWK = {
|
|
29
|
+
kty: 'EC',
|
|
30
|
+
crv: 'P-256',
|
|
31
|
+
x: DEV_PRIVATE_JWK.x,
|
|
32
|
+
y: DEV_PRIVATE_JWK.y,
|
|
33
|
+
use: 'sig',
|
|
34
|
+
alg: DEV_ALG,
|
|
35
|
+
kid: DEV_KID,
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAE7C,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,sEAAsE;AACtE,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,OAAO;IACZ,CAAC,EAAE,6CAA6C;IAChD,CAAC,EAAE,6CAA6C;IAChD,CAAC,EAAE,6CAA6C;CACxC,CAAC;AAEX,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,GAAG,EAAE,IAAI;IACT,GAAG,EAAE,OAAO;IACZ,CAAC,EAAE,eAAe,CAAC,CAAC;IACpB,CAAC,EAAE,eAAe,CAAC,CAAC;IACpB,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;CACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cast the picker shows — the ONE fact the issuer and the app it authenticates for
|
|
3
|
+
* must agree on.
|
|
4
|
+
*
|
|
5
|
+
* `sub` is the join. The issuer mints ID tokens carrying it; the vertical's seed links it
|
|
6
|
+
* to a principal in its identity directory (`linkIdentity`). Both read the same array, so
|
|
7
|
+
* neither can drift from the other, and both do the ordinary production thing: an issuer
|
|
8
|
+
* asserts a subject, a directory maps it to a principal.
|
|
9
|
+
*
|
|
10
|
+
* Keep `sub` values stable and human-readable (`dev|anna`) — they end up in a seeded
|
|
11
|
+
* directory that survives across restarts, and a reader looking at an identity link should
|
|
12
|
+
* be able to tell who it is.
|
|
13
|
+
*/
|
|
14
|
+
export interface DevPersona {
|
|
15
|
+
/** The OIDC `sub` this persona authenticates as. Stable across restarts. */
|
|
16
|
+
sub: string;
|
|
17
|
+
/** Shown in the picker, and sent as the `name` claim. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Sent as the `email` claim. */
|
|
20
|
+
email: string;
|
|
21
|
+
/** A one-line hint under the name in the picker — "office-admin", "portal customer @ …". */
|
|
22
|
+
note?: string;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=personas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personas.d.ts","sourceRoot":"","sources":["../src/personas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,IAAI,CAAC,EAAE,MAAM,CAAC;CACf"}
|
package/dist/personas.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personas.js","sourceRoot":"","sources":["../src/personas.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@substrat-run/dev-issuer",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A local OpenID Connect provider whose only shortcut is that you pick a user instead of typing a password. Dev only \u2014 never deployed, never a dependency of a vertical's runtime.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/substrat-run/substrat.git",
|
|
9
|
+
"directory": "packages/dev-issuer"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"substrat-dev-issuer": "./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"serve": "tsx src/cli.ts"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@hono/node-server": "^1.13.0",
|
|
34
|
+
"@substrat-run/contracts": "workspace:^",
|
|
35
|
+
"@substrat-run/vertical-auth": "workspace:^",
|
|
36
|
+
"jose": "^6.2.3"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"hono": "^4.6.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@substrat-run/oidc-rp": "workspace:^",
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"hono": "^4.6.0",
|
|
45
|
+
"tsx": "^4.19.0",
|
|
46
|
+
"typescript": "^7.0.0",
|
|
47
|
+
"vitest": "catalog:"
|
|
48
|
+
}
|
|
49
|
+
}
|