@camstack/server 1.2.486 → 1.2.487
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/agent/agent-mesh-join.js +80 -0
- package/dist/agent/main.js +61 -2
- package/dist/api/auth-whoami.js +1 -1
- package/dist/api/core/auth-admin-tokens.procedures.js +138 -0
- package/dist/api/core/auth-limits.js +77 -0
- package/dist/api/core/auth-reauth.procedures.js +108 -0
- package/dist/api/core/auth-sessions.procedures.js +166 -0
- package/dist/api/core/auth.router.js +281 -80
- package/dist/api/health/adaptive-probe.routes.js +1 -1
- package/dist/api/trpc/core-cap-bridge.js +47 -5
- package/dist/api/trpc/generated-cap-mounts.js +19 -1
- package/dist/api/trpc/generated-cap-routers.js +512 -205
- package/dist/api/trpc/trpc.context.js +86 -34
- package/dist/api/trpc/trpc.middleware.js +8 -11
- package/dist/api/trpc/trpc.router.js +6 -1
- package/dist/auth/auth-network-policy.js +75 -0
- package/dist/auth/auth-rate-limit.js +25 -20
- package/dist/auth/client-address.js +68 -0
- package/dist/auth/external-link-intents.js +12 -3
- package/dist/auth/hub-sessions.js +74 -0
- package/dist/auth/native-link-completions.js +68 -0
- package/dist/auth/native-return.js +33 -0
- package/dist/auth/session-lifetimes.js +46 -0
- package/dist/auth/session-mint.js +110 -13
- package/dist/auth/sso-finish-deps.js +56 -1
- package/dist/auth/sso-finish.js +132 -17
- package/dist/auth/sso-routes.js +95 -0
- package/dist/auth/used-bridge-ids.js +52 -0
- package/dist/core/addon/addon-registry.service.js +5 -0
- package/dist/core/agent/agent-registry.service.js +10 -0
- package/dist/core/auth/auth.service.js +50 -1
- package/dist/core/auth/handoff-code.service.js +9 -3
- package/dist/core/moleculer/hub-mesh-guard.js +132 -0
- package/dist/core/moleculer/moleculer.service.js +33 -0
- package/dist/main.js +92 -28
- package/package.json +13 -13
|
@@ -8,58 +8,71 @@
|
|
|
8
8
|
* JWT signing stays in the server — it's a transport-level concern
|
|
9
9
|
* (the server owns the secret and the HTTP session).
|
|
10
10
|
*
|
|
11
|
-
* Pre-auth login surfaces (OIDC
|
|
11
|
+
* Pre-auth login surfaces (OIDC redirect buttons + the
|
|
12
12
|
* passkey second-factor widget) are aggregated for the login page by the
|
|
13
13
|
* public `listLoginMethods` procedure from the `login-method` cap
|
|
14
14
|
* collection — the single, generic mechanism every auth addon contributes
|
|
15
15
|
* to (superseding the removed `auth.listProviders`).
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.EXCHANGE_SESSION_HEADER = void 0;
|
|
18
|
+
exports.EXCHANGE_SESSION_HEADER = exports.BROWSER_HANDOFF_PATH = void 0;
|
|
19
19
|
exports.createAuthRouter = createAuthRouter;
|
|
20
20
|
const types_1 = require("@camstack/types");
|
|
21
21
|
const server_1 = require("@trpc/server");
|
|
22
22
|
const zod_1 = require("zod");
|
|
23
|
+
const auth_network_policy_js_1 = require("../../auth/auth-network-policy.js");
|
|
23
24
|
const auth_rate_limit_js_1 = require("../../auth/auth-rate-limit.js");
|
|
24
25
|
const auth_provider_gate_js_1 = require("../../auth/auth-provider-gate.js");
|
|
25
26
|
const external_link_intents_js_1 = require("../../auth/external-link-intents.js");
|
|
26
27
|
const passkey_provider_js_1 = require("../../auth/passkey-provider.js");
|
|
27
28
|
const second_factors_js_1 = require("../../auth/second-factors.js");
|
|
28
29
|
const session_cookie_js_1 = require("../../auth/session-cookie.js");
|
|
30
|
+
const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
|
|
29
31
|
const session_mint_js_1 = require("../../auth/session-mint.js");
|
|
30
32
|
const handoff_code_service_js_1 = require("../../core/auth/handoff-code.service.js");
|
|
31
33
|
const share_token_service_js_1 = require("../../core/auth/share-token.service.js");
|
|
32
34
|
const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
|
|
33
35
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const CEREMONY_LIMITER = (0, auth_rate_limit_js_1.createRateLimiter)({ windowMs: 60_000, max: 30 });
|
|
44
|
-
function makeRateLimitedProcedure(limiter) {
|
|
36
|
+
const auth_limits_js_1 = require("./auth-limits.js");
|
|
37
|
+
const auth_admin_tokens_procedures_js_1 = require("./auth-admin-tokens.procedures.js");
|
|
38
|
+
const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
|
|
39
|
+
const auth_sessions_procedures_js_1 = require("./auth-sessions.procedures.js");
|
|
40
|
+
/**
|
|
41
|
+
* A public procedure limited per (procedure, REAL client address) — see
|
|
42
|
+
* `auth-limits.ts` for the tiers and why the address is the forwarded client.
|
|
43
|
+
*/
|
|
44
|
+
function makeRateLimitedProcedure(limiter, network) {
|
|
45
45
|
return trpc_middleware_js_1.publicProcedure.use(({ ctx, path, next }) => {
|
|
46
|
-
|
|
47
|
-
if (key !== null) {
|
|
48
|
-
const verdict = limiter.check(`${path}:${key}`);
|
|
49
|
-
if (!verdict.allowed) {
|
|
50
|
-
throw new server_1.TRPCError({
|
|
51
|
-
code: 'TOO_MANY_REQUESTS',
|
|
52
|
-
message: `Too many attempts — retry in ${verdict.retryAfterSeconds}s`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
46
|
+
(0, auth_limits_js_1.limitByClient)(limiter, path, ctx.req, network);
|
|
56
47
|
return next({ ctx });
|
|
57
48
|
});
|
|
58
49
|
}
|
|
59
|
-
/**
|
|
60
|
-
const
|
|
61
|
-
/**
|
|
62
|
-
|
|
50
|
+
/** Asks for the refresh-token flow (short access token + rotating refresh token). */
|
|
51
|
+
const RefreshableInput = zod_1.z.boolean().optional();
|
|
52
|
+
/** The request's User-Agent, trimmed, for the sessions list. */
|
|
53
|
+
function clientLabel(req) {
|
|
54
|
+
const ua = req?.headers['user-agent'];
|
|
55
|
+
const value = Array.isArray(ua) ? ua[0] : ua;
|
|
56
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, 200) : undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Signed-in legs that mint or redeem a one-time code for the CALLER (native
|
|
60
|
+
* link claim, browser handoff — D801): per user, not per ip, because the
|
|
61
|
+
* caller is known and a tunnel puts every client behind one address.
|
|
62
|
+
*/
|
|
63
|
+
const SELF_CODE_LIMITER = (0, auth_rate_limit_js_1.createRateLimiter)({ windowMs: 60_000, max: 10 });
|
|
64
|
+
const selfCodeProcedure = trpc_middleware_js_1.protectedProcedure.use(({ ctx, path, next }) => {
|
|
65
|
+
const verdict = SELF_CODE_LIMITER.check(`${path}:${ctx.user?.id ?? 'anonymous'}`);
|
|
66
|
+
if (!verdict.allowed) {
|
|
67
|
+
throw new server_1.TRPCError({
|
|
68
|
+
code: 'TOO_MANY_REQUESTS',
|
|
69
|
+
message: `Too many attempts — retry in ${verdict.retryAfterSeconds}s`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return next({ ctx });
|
|
73
|
+
});
|
|
74
|
+
/** The hub route a browser-handoff code is redeemed at (`main.ts`). */
|
|
75
|
+
exports.BROWSER_HANDOFF_PATH = '/api/auth/browser-handoff';
|
|
63
76
|
/**
|
|
64
77
|
* The available second-factor kinds a user may satisfy after the
|
|
65
78
|
* password leg. Capability-driven — `totp` maps to the
|
|
@@ -92,12 +105,23 @@ const LoginResultSchema = zod_1.z.object({
|
|
|
92
105
|
}),
|
|
93
106
|
requiresTotp: zod_1.z.boolean().optional(),
|
|
94
107
|
secondFactors: zod_1.z.array(second_factors_js_1.SecondFactorSchema).optional(),
|
|
108
|
+
/** When the access token expires (epoch ms). Absent on a challenge. */
|
|
109
|
+
expiresAt: zod_1.z.number().optional(),
|
|
110
|
+
/** The server-side login session. */
|
|
111
|
+
sessionId: zod_1.z.string().optional(),
|
|
112
|
+
/** When the session expires unless refreshed (epoch ms). */
|
|
113
|
+
sessionExpiresAt: zod_1.z.number().optional(),
|
|
114
|
+
/**
|
|
115
|
+
* Only when the client asked (`refreshable: true`): swap it at
|
|
116
|
+
* `auth.refreshSession` before `expiresAt`. Store it like a password.
|
|
117
|
+
*/
|
|
118
|
+
refreshToken: zod_1.z.string().optional(),
|
|
95
119
|
});
|
|
96
120
|
// ── Login-method aggregation (login-method cap) ──────────────────────
|
|
97
121
|
//
|
|
98
122
|
// The PUBLIC `listLoginMethods` procedure walks the `login-method`
|
|
99
123
|
// collection and returns the union that the login page renders directly:
|
|
100
|
-
// `redirect` buttons (OIDC
|
|
124
|
+
// `redirect` buttons (OIDC), native `passkey` ceremonies, and
|
|
101
125
|
// (generic mechanism, unused by shipped addons) pre-auth `widget`s.
|
|
102
126
|
// The widget arm is enriched server-side with a public `bundleUrl` from
|
|
103
127
|
// `addonId` + `bundle`, so the addon never encodes the static-route
|
|
@@ -177,7 +201,9 @@ function toShareTokenSummary(record) {
|
|
|
177
201
|
* a leaked restricted token would otherwise widen its own reach.
|
|
178
202
|
*/
|
|
179
203
|
function assertRealUserSession(user, what = 'Share-token management') {
|
|
180
|
-
|
|
204
|
+
// An admin token is a script's credential, never a person's session: it
|
|
205
|
+
// must not turn itself into a session (handoff codes) or mint links.
|
|
206
|
+
if (user.isScoped || user.shareView || user.adminTokenId !== undefined) {
|
|
181
207
|
throw new server_1.TRPCError({
|
|
182
208
|
code: 'FORBIDDEN',
|
|
183
209
|
message: `${what} requires a real user session`,
|
|
@@ -246,7 +272,6 @@ const MeSchema = zod_1.z
|
|
|
246
272
|
allowedDevices: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
247
273
|
}),
|
|
248
274
|
isApiKey: zod_1.z.boolean(),
|
|
249
|
-
agentId: zod_1.z.string().optional(),
|
|
250
275
|
/**
|
|
251
276
|
* The caller's resolved effective scope. Absent only on the (unreachable
|
|
252
277
|
* here — `me` is `protected`) null-principal branch.
|
|
@@ -254,7 +279,13 @@ const MeSchema = zod_1.z
|
|
|
254
279
|
scope: EffectiveScopeSchema.optional(),
|
|
255
280
|
})
|
|
256
281
|
.nullable();
|
|
257
|
-
function createAuthRouter(auth, registry, moleculer = null, shareTokens = null, handoffCodes = new handoff_code_service_js_1.HandoffCodeService(), linkIntents = new external_link_intents_js_1.ExternalLinkIntents()) {
|
|
282
|
+
function createAuthRouter(auth, registry, moleculer = null, shareTokens = null, handoffCodes = new handoff_code_service_js_1.HandoffCodeService(), linkIntents = new external_link_intents_js_1.ExternalLinkIntents(), nativeLink = null, options = {}) {
|
|
283
|
+
const network = options.network ?? auth_network_policy_js_1.DEFAULT_AUTH_NETWORK_POLICY;
|
|
284
|
+
const limiters = options.limiters ?? auth_limits_js_1.PROCESS_AUTH_LIMITERS;
|
|
285
|
+
/** Secret-verifying legs (password / TOTP / passkey assertion / codes). */
|
|
286
|
+
const credentialProcedure = makeRateLimitedProcedure(limiters.credential, network);
|
|
287
|
+
/** Challenge-minting begin-legs — looser, they verify nothing. */
|
|
288
|
+
const ceremonyProcedure = makeRateLimitedProcedure(limiters.ceremony, network);
|
|
258
289
|
const requireShareTokens = () => {
|
|
259
290
|
if (!shareTokens) {
|
|
260
291
|
throw new Error('Share tokens unavailable — service not wired on this node');
|
|
@@ -271,9 +302,21 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
271
302
|
const minter = (0, session_mint_js_1.createSessionMinter)({
|
|
272
303
|
signer: auth,
|
|
273
304
|
directory: () => registry?.getSingleton('user-management'),
|
|
305
|
+
...(options.sessions !== undefined ? { sessions: options.sessions } : {}),
|
|
274
306
|
});
|
|
275
307
|
const findUserById = minter.findUser;
|
|
276
|
-
|
|
308
|
+
/** How THIS request's login is recorded on its session. */
|
|
309
|
+
const issueFor = (req, method, refreshable) => {
|
|
310
|
+
const client = clientLabel(req);
|
|
311
|
+
const ip = network.clientAddress(req).ip;
|
|
312
|
+
return {
|
|
313
|
+
method,
|
|
314
|
+
refreshable: refreshable === true,
|
|
315
|
+
...(client !== undefined ? { client } : {}),
|
|
316
|
+
...(ip !== null ? { ip } : {}),
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
const mintSessionForUserId = (userId, issue) => minter.mintForUserId(userId, issue);
|
|
277
320
|
/**
|
|
278
321
|
* `mintSessionForUserId` with the "user vanished" failure mapped to a
|
|
279
322
|
* clean 401 — for the token-exchange legs (`exchangeSession`,
|
|
@@ -281,9 +324,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
281
324
|
* backing user may have been deleted mid-flight. Infrastructure
|
|
282
325
|
* failures (user-management cap not registered) still surface as 500.
|
|
283
326
|
*/
|
|
284
|
-
const mintSessionOrUnauthorized = async (userId) => {
|
|
327
|
+
const mintSessionOrUnauthorized = async (userId, issue) => {
|
|
285
328
|
try {
|
|
286
|
-
return await mintSessionForUserId(userId);
|
|
329
|
+
return await mintSessionForUserId(userId, issue);
|
|
287
330
|
}
|
|
288
331
|
catch (error) {
|
|
289
332
|
if (error instanceof session_mint_js_1.SessionUserGoneError) {
|
|
@@ -302,6 +345,30 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
302
345
|
passkeyProvider: () => (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer),
|
|
303
346
|
}, userId, primary);
|
|
304
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* The second factors THIS login owes: none when the operator listed a
|
|
350
|
+
* trusted network, the request came DIRECTLY from it (never through the
|
|
351
|
+
* tunnel or any proxy — `auth-network-policy.ts`), and the user has
|
|
352
|
+
* factors to skip. Every skip is audited.
|
|
353
|
+
*/
|
|
354
|
+
const factorsOwedHere = async (userId, primary, req) => {
|
|
355
|
+
const factors = await collectSecondFactorsFor(userId, primary);
|
|
356
|
+
if (factors.length === 0)
|
|
357
|
+
return factors;
|
|
358
|
+
const address = network.clientAddress(req);
|
|
359
|
+
if (!network.isTrustedNetworkLogin(address))
|
|
360
|
+
return factors;
|
|
361
|
+
const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
|
|
362
|
+
if (internal === null)
|
|
363
|
+
return factors; // cannot audit it → do not skip it
|
|
364
|
+
await internal.audit.record({
|
|
365
|
+
action: 'second-factor-skipped',
|
|
366
|
+
userId,
|
|
367
|
+
...(address.ip !== null ? { ip: address.ip } : {}),
|
|
368
|
+
detail: { method: primary, factors: factors.join(',') },
|
|
369
|
+
});
|
|
370
|
+
return [];
|
|
371
|
+
};
|
|
305
372
|
/**
|
|
306
373
|
* The login page's methods: every enabled auth addon's `login-method`
|
|
307
374
|
* contributions (D777), validated and deduplicated. Shared by
|
|
@@ -312,7 +379,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
312
379
|
return [];
|
|
313
380
|
const out = [];
|
|
314
381
|
// Defense-in-depth dedupe: a routing bug in a grouped runner once
|
|
315
|
-
// resolved every provider to the same addon (3× identical
|
|
382
|
+
// resolved every provider to the same addon (3× identical login
|
|
316
383
|
// buttons) — a duplicated contribution id must never render twice.
|
|
317
384
|
const seenIds = new Set();
|
|
318
385
|
for (const [addonId, provider] of registry.getCollectionEntries('login-method')) {
|
|
@@ -379,9 +446,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
379
446
|
: []);
|
|
380
447
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
381
448
|
login: credentialProcedure
|
|
382
|
-
.input(zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string() }))
|
|
449
|
+
.input(zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string(), refreshable: RefreshableInput }))
|
|
383
450
|
.output(LoginResultSchema)
|
|
384
|
-
.mutation(async ({ input }) => {
|
|
451
|
+
.mutation(async ({ input, ctx }) => {
|
|
385
452
|
const userMgmt = registry?.getSingleton('user-management');
|
|
386
453
|
if (!userMgmt) {
|
|
387
454
|
// The local-auth addon owns this cap. When this fires the
|
|
@@ -393,12 +460,15 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
393
460
|
'The `local-auth` addon failed to initialize; check server logs ' +
|
|
394
461
|
'for an error tagged `[hub/local-auth]`.');
|
|
395
462
|
}
|
|
463
|
+
(0, auth_limits_js_1.assertAccountNotLocked)(limiters, input.username);
|
|
396
464
|
const user = await userMgmt.validateCredentials({
|
|
397
465
|
username: input.username,
|
|
398
466
|
password: input.password,
|
|
399
467
|
});
|
|
400
|
-
if (!user)
|
|
468
|
+
if (!user) {
|
|
469
|
+
(0, auth_limits_js_1.recordAccountFailure)(limiters, input.username);
|
|
401
470
|
throw new Error('Invalid credentials');
|
|
471
|
+
}
|
|
402
472
|
// ── Second-factor gate ───────────────────────────────────────
|
|
403
473
|
// After credentials validate, collect every ACTIVE second
|
|
404
474
|
// factor (TOTP when enrolled; passkey only when enrolled AND
|
|
@@ -410,12 +480,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
410
480
|
// protected endpoints (the auth middleware rejects anything
|
|
411
481
|
// without the standard session shape); the same token binds
|
|
412
482
|
// both the TOTP and the passkey second legs (same userId).
|
|
413
|
-
const secondFactors = await
|
|
483
|
+
const secondFactors = await factorsOwedHere(user.id, 'password', ctx.req);
|
|
414
484
|
if (secondFactors.length > 0) {
|
|
415
485
|
const challengeToken = auth.signTotpChallengeToken({
|
|
416
486
|
userId: user.id,
|
|
417
487
|
username: user.username,
|
|
418
488
|
isAdmin: user.isAdmin,
|
|
489
|
+
method: 'password',
|
|
419
490
|
});
|
|
420
491
|
return {
|
|
421
492
|
token: challengeToken,
|
|
@@ -425,7 +496,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
425
496
|
};
|
|
426
497
|
}
|
|
427
498
|
// `user` is the record `validateCredentials` just read — fresh.
|
|
428
|
-
return minter.mintForRecord(user);
|
|
499
|
+
return minter.mintForRecord(user, issueFor(ctx.req, 'password', input.refreshable));
|
|
429
500
|
}),
|
|
430
501
|
/**
|
|
431
502
|
* Second leg of the 2FA login. Accepts the challenge token minted
|
|
@@ -441,9 +512,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
441
512
|
* deleted concurrently — rare).
|
|
442
513
|
*/
|
|
443
514
|
loginVerifyTotp: credentialProcedure
|
|
444
|
-
.input(zod_1.z.object({ challengeToken: zod_1.z.string(), code: zod_1.z.string() }))
|
|
515
|
+
.input(zod_1.z.object({ challengeToken: zod_1.z.string(), code: zod_1.z.string(), refreshable: RefreshableInput }))
|
|
445
516
|
.output(LoginResultSchema)
|
|
446
|
-
.mutation(async ({ input }) => {
|
|
517
|
+
.mutation(async ({ input, ctx }) => {
|
|
447
518
|
const claims = auth.verifyTotpChallengeToken(input.challengeToken);
|
|
448
519
|
if (!claims) {
|
|
449
520
|
throw new Error('Invalid or expired TOTP challenge — please re-enter your password');
|
|
@@ -452,13 +523,15 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
452
523
|
if (!userMgmt) {
|
|
453
524
|
throw new Error('Login unavailable — `user-management` capability not registered');
|
|
454
525
|
}
|
|
526
|
+
(0, auth_limits_js_1.assertAccountNotLocked)(limiters, claims.username);
|
|
455
527
|
const verify = typeof userMgmt.verifyTotp === 'function'
|
|
456
528
|
? await userMgmt.verifyTotp({ userId: claims.userId, code: input.code.trim() })
|
|
457
529
|
: { valid: false };
|
|
458
530
|
if (!verify.valid) {
|
|
531
|
+
(0, auth_limits_js_1.recordAccountFailure)(limiters, claims.username);
|
|
459
532
|
throw new Error('Invalid TOTP code');
|
|
460
533
|
}
|
|
461
|
-
return mintSessionForUserId(claims.userId);
|
|
534
|
+
return mintSessionForUserId(claims.userId, issueFor(ctx.req, claims.method ?? 'password', input.refreshable));
|
|
462
535
|
}),
|
|
463
536
|
/**
|
|
464
537
|
* Passkey second leg — step 1 of 2. Accepts the challenge token
|
|
@@ -488,9 +561,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
488
561
|
* `loginVerifyTotp` uses (fresh user re-fetch for up-to-date scopes).
|
|
489
562
|
*/
|
|
490
563
|
loginVerifyPasskey: credentialProcedure
|
|
491
|
-
.input(zod_1.z.object({
|
|
564
|
+
.input(zod_1.z.object({
|
|
565
|
+
challengeToken: zod_1.z.string(),
|
|
566
|
+
response: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
|
|
567
|
+
refreshable: RefreshableInput,
|
|
568
|
+
}))
|
|
492
569
|
.output(LoginResultSchema)
|
|
493
|
-
.mutation(async ({ input }) => {
|
|
570
|
+
.mutation(async ({ input, ctx }) => {
|
|
494
571
|
const claims = auth.verifyTotpChallengeToken(input.challengeToken);
|
|
495
572
|
if (!claims) {
|
|
496
573
|
throw new Error('Invalid or expired login challenge — please re-enter your password');
|
|
@@ -498,14 +575,16 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
498
575
|
const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
|
|
499
576
|
if (!provider)
|
|
500
577
|
throw new Error('Passkey authentication is not available');
|
|
578
|
+
(0, auth_limits_js_1.assertAccountNotLocked)(limiters, claims.username);
|
|
501
579
|
const result = await provider.finishAuthentication({
|
|
502
580
|
userId: claims.userId,
|
|
503
581
|
response: input.response,
|
|
504
582
|
});
|
|
505
583
|
if (!result.verified) {
|
|
584
|
+
(0, auth_limits_js_1.recordAccountFailure)(limiters, claims.username);
|
|
506
585
|
throw new Error('Passkey verification failed');
|
|
507
586
|
}
|
|
508
|
-
return mintSessionForUserId(claims.userId);
|
|
587
|
+
return mintSessionForUserId(claims.userId, issueFor(ctx.req, claims.method ?? 'password', input.refreshable));
|
|
509
588
|
}),
|
|
510
589
|
/**
|
|
511
590
|
* Usernameless (passkey-FIRST) login — step 1 of 2. PUBLIC. Returns
|
|
@@ -515,9 +594,8 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
515
594
|
* — no username, no password. The challenge is minted and stored by
|
|
516
595
|
* the `user-passkeys` provider, deliberately NOT bound to any user.
|
|
517
596
|
*
|
|
518
|
-
* Abuse surface:
|
|
519
|
-
*
|
|
520
|
-
* by its 5-min TTL prune.
|
|
597
|
+
* Abuse surface: limited per client address (ceremony tier); the
|
|
598
|
+
* challenge store is bounded by its 5-min TTL prune.
|
|
521
599
|
*/
|
|
522
600
|
passkeyLoginBegin: ceremonyProcedure
|
|
523
601
|
.input(zod_1.z.void())
|
|
@@ -544,9 +622,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
544
622
|
* re-proving it adds nothing.
|
|
545
623
|
*/
|
|
546
624
|
passkeyLoginFinish: credentialProcedure
|
|
547
|
-
.input(zod_1.z.object({ response: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) }))
|
|
625
|
+
.input(zod_1.z.object({ response: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()), refreshable: RefreshableInput }))
|
|
548
626
|
.output(LoginResultSchema)
|
|
549
|
-
.mutation(async ({ input }) => {
|
|
627
|
+
.mutation(async ({ input, ctx }) => {
|
|
550
628
|
const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
|
|
551
629
|
if (!provider)
|
|
552
630
|
throw new Error('Passkey authentication is not available');
|
|
@@ -556,13 +634,14 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
556
634
|
if (!result.verified || !result.userId) {
|
|
557
635
|
throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Passkey verification failed' });
|
|
558
636
|
}
|
|
559
|
-
const secondFactors = await
|
|
637
|
+
const secondFactors = await factorsOwedHere(result.userId, 'passkey', ctx.req);
|
|
560
638
|
if (secondFactors.length > 0) {
|
|
561
639
|
const challenged = await findUserById(result.userId);
|
|
562
640
|
const challengeToken = auth.signTotpChallengeToken({
|
|
563
641
|
userId: challenged.id,
|
|
564
642
|
username: challenged.username,
|
|
565
643
|
isAdmin: challenged.isAdmin,
|
|
644
|
+
method: 'passkey',
|
|
566
645
|
});
|
|
567
646
|
return {
|
|
568
647
|
token: challengeToken,
|
|
@@ -575,7 +654,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
575
654
|
secondFactors,
|
|
576
655
|
};
|
|
577
656
|
}
|
|
578
|
-
return mintSessionForUserId(result.userId);
|
|
657
|
+
return mintSessionForUserId(result.userId, issueFor(ctx.req, 'passkey', input.refreshable));
|
|
579
658
|
}),
|
|
580
659
|
/**
|
|
581
660
|
* PUBLIC — upgrade the browser's httpOnly session COOKIE into a
|
|
@@ -598,9 +677,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
598
677
|
* scopes), equivalent to what `auth.login` hands out.
|
|
599
678
|
*/
|
|
600
679
|
exchangeSession: credentialProcedure
|
|
601
|
-
.input(zod_1.z.object({}).optional())
|
|
680
|
+
.input(zod_1.z.object({ refreshable: RefreshableInput }).optional())
|
|
602
681
|
.output(LoginResultSchema)
|
|
603
|
-
.mutation(async ({ ctx }) => {
|
|
682
|
+
.mutation(async ({ ctx, input }) => {
|
|
604
683
|
if (readRequestHeader(ctx.req, exports.EXCHANGE_SESSION_HEADER) !== '1') {
|
|
605
684
|
throw new server_1.TRPCError({
|
|
606
685
|
code: 'FORBIDDEN',
|
|
@@ -627,7 +706,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
627
706
|
typeof payload.userId !== 'string') {
|
|
628
707
|
throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid session cookie' });
|
|
629
708
|
}
|
|
630
|
-
|
|
709
|
+
// A session of its own for the client that asked (the hub-served
|
|
710
|
+
// viewer): signing out of one never signs the other out.
|
|
711
|
+
return mintSessionOrUnauthorized(payload.userId, issueFor(ctx.req, 'exchange', input?.refreshable));
|
|
631
712
|
}),
|
|
632
713
|
// ── One-time handoff codes (native-app login handoff) ─────────────
|
|
633
714
|
//
|
|
@@ -640,18 +721,24 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
640
721
|
* sessions only — scoped (`cst_`) and share-view (`csv_`) callers
|
|
641
722
|
* must never convert themselves into a full session. */
|
|
642
723
|
createHandoffCode: trpc_middleware_js_1.protectedProcedure
|
|
643
|
-
.input(zod_1.z.
|
|
724
|
+
.input(zod_1.z.object({ purpose: zod_1.z.enum(['renew', 'new-session']).optional() }).optional())
|
|
644
725
|
.output(zod_1.z.object({ code: zod_1.z.string(), expiresAt: zod_1.z.number() }))
|
|
645
|
-
.mutation(({ ctx }) => {
|
|
726
|
+
.mutation(({ ctx, input }) => {
|
|
646
727
|
assertRealUserSession(ctx.user, 'Handoff-code minting');
|
|
647
|
-
|
|
728
|
+
// `renew` (the default — what installed viewer apps call this for)
|
|
729
|
+
// continues THIS session; `new-session` (signing the native app in)
|
|
730
|
+
// opens a separate one for the redeemer.
|
|
731
|
+
return handoffCodes.create(ctx.user.id, {
|
|
732
|
+
...(ctx.user.sessionId !== undefined ? { sessionId: ctx.user.sessionId } : {}),
|
|
733
|
+
purpose: input?.purpose ?? 'renew',
|
|
734
|
+
});
|
|
648
735
|
}),
|
|
649
736
|
/** PUBLIC — redeem a one-time handoff code for a session bearer.
|
|
650
737
|
* Unknown / expired / already-used codes → UNAUTHORIZED. */
|
|
651
738
|
redeemHandoffCode: credentialProcedure
|
|
652
|
-
.input(zod_1.z.object({ code: zod_1.z.string().min(1) }))
|
|
739
|
+
.input(zod_1.z.object({ code: zod_1.z.string().min(1), refreshable: RefreshableInput }))
|
|
653
740
|
.output(LoginResultSchema)
|
|
654
|
-
.mutation(async ({ input }) => {
|
|
741
|
+
.mutation(async ({ input, ctx }) => {
|
|
655
742
|
const grant = handoffCodes.redeem(input.code);
|
|
656
743
|
if (!grant) {
|
|
657
744
|
throw new server_1.TRPCError({
|
|
@@ -659,7 +746,24 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
659
746
|
message: 'Invalid or expired handoff code',
|
|
660
747
|
});
|
|
661
748
|
}
|
|
662
|
-
|
|
749
|
+
if (grant.purpose === 'renew' && grant.sessionId !== undefined) {
|
|
750
|
+
// A legacy client renewing: the SAME session, its expiry slides. A
|
|
751
|
+
// session revoked meanwhile (logout elsewhere) renews nothing.
|
|
752
|
+
try {
|
|
753
|
+
return await minter.continueSession(grant.userId, grant.sessionId);
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
if (error instanceof session_mint_js_1.SessionRefreshRefusedError ||
|
|
757
|
+
error instanceof session_mint_js_1.SessionUserGoneError) {
|
|
758
|
+
throw new server_1.TRPCError({
|
|
759
|
+
code: 'UNAUTHORIZED',
|
|
760
|
+
message: 'Session expired — sign in again',
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return mintSessionOrUnauthorized(grant.userId, issueFor(ctx.req, 'handoff', input.refreshable));
|
|
663
767
|
}),
|
|
664
768
|
me: trpc_middleware_js_1.protectedProcedure
|
|
665
769
|
.input(zod_1.z.void())
|
|
@@ -735,7 +839,16 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
735
839
|
});
|
|
736
840
|
if (!ok)
|
|
737
841
|
throw new Error('Current password is incorrect');
|
|
738
|
-
|
|
842
|
+
// Hub-internal: the password changes AND every other session of the
|
|
843
|
+
// user ends (this one stays signed in).
|
|
844
|
+
const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
|
|
845
|
+
if (internal === null)
|
|
846
|
+
throw new Error('user-management capability not available');
|
|
847
|
+
await internal.setPassword({
|
|
848
|
+
userId: ctx.user.id,
|
|
849
|
+
newPassword: input.newPassword,
|
|
850
|
+
...(ctx.user.sessionId !== undefined ? { keepSessionId: ctx.user.sessionId } : {}),
|
|
851
|
+
});
|
|
739
852
|
return { success: true };
|
|
740
853
|
}),
|
|
741
854
|
setupOwnTotp: trpc_middleware_js_1.protectedProcedure
|
|
@@ -761,9 +874,11 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
761
874
|
return userMgmt.confirmTotp({ userId: ctx.user.id, code: input.code });
|
|
762
875
|
}),
|
|
763
876
|
disableOwnTotp: trpc_middleware_js_1.protectedProcedure
|
|
764
|
-
.
|
|
877
|
+
// Re-authentication required: a session alone must not strip 2FA.
|
|
878
|
+
.input(zod_1.z.object({ reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput }).optional())
|
|
765
879
|
.output(zod_1.z.object({ success: zod_1.z.literal(true) }))
|
|
766
|
-
.mutation(async ({ ctx }) => {
|
|
880
|
+
.mutation(async ({ ctx, input }) => {
|
|
881
|
+
(0, auth_reauth_procedures_js_1.assertReauthenticated)(auth, ctx.user, input?.reauthToken);
|
|
767
882
|
const userMgmt = registry?.getSingleton('user-management');
|
|
768
883
|
if (!userMgmt)
|
|
769
884
|
throw new Error('user-management capability not available');
|
|
@@ -831,11 +946,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
831
946
|
}
|
|
832
947
|
}),
|
|
833
948
|
removeOwnPasskey: trpc_middleware_js_1.protectedProcedure
|
|
834
|
-
|
|
949
|
+
// Re-authentication required: removing a passkey can strip a factor.
|
|
950
|
+
.input(zod_1.z.object({ credentialId: zod_1.z.string(), reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput.optional() }))
|
|
835
951
|
.output(zod_1.z.object({ success: zod_1.z.literal(true) }))
|
|
836
952
|
.mutation(async ({ input, ctx }) => {
|
|
837
953
|
if (!ctx.user)
|
|
838
954
|
throw new Error('Not authenticated');
|
|
955
|
+
(0, auth_reauth_procedures_js_1.assertReauthenticated)(auth, ctx.user, input.reauthToken);
|
|
839
956
|
const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
|
|
840
957
|
if (!provider)
|
|
841
958
|
throw new Error('Passkey management is not available');
|
|
@@ -939,22 +1056,30 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
939
1056
|
});
|
|
940
1057
|
}
|
|
941
1058
|
}),
|
|
942
|
-
/**
|
|
1059
|
+
/**
|
|
1060
|
+
* Share links. `owner: 'self'` — the caller's own (My access);
|
|
1061
|
+
* `owner: 'all'` — every user's, admin only. Omitted keeps the original
|
|
1062
|
+
* answer (own for a user, all for an admin) for clients that predate it.
|
|
1063
|
+
*/
|
|
943
1064
|
listShareTokens: trpc_middleware_js_1.protectedProcedure
|
|
944
|
-
.input(zod_1.z.
|
|
1065
|
+
.input(zod_1.z.object({ owner: zod_1.z.enum(['self', 'all']).optional() }).optional())
|
|
945
1066
|
.output(zod_1.z.array(ShareTokenSummarySchema))
|
|
946
|
-
.query(async ({ ctx }) => {
|
|
1067
|
+
.query(async ({ ctx, input }) => {
|
|
947
1068
|
assertRealUserSession(ctx.user);
|
|
948
1069
|
const service = requireShareTokens();
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
1070
|
+
const owner = input?.owner ?? (ctx.user.isAdmin ? 'all' : 'self');
|
|
1071
|
+
if (owner === 'all' && !ctx.user.isAdmin) {
|
|
1072
|
+
throw new server_1.TRPCError({
|
|
1073
|
+
code: 'FORBIDDEN',
|
|
1074
|
+
message: "Listing every user's share links requires an admin",
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const records = owner === 'all' ? await service.listAll() : await service.listForUser(ctx.user.id);
|
|
952
1078
|
return records.map(toShareTokenSummary);
|
|
953
1079
|
}),
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
.mutation(() => ({ success: true })),
|
|
1080
|
+
...(0, auth_sessions_procedures_js_1.createSessionProcedures)({ registry, minter, credentialProcedure }),
|
|
1081
|
+
...(0, auth_reauth_procedures_js_1.createReauthProcedures)({ auth, registry, moleculer, limiters }),
|
|
1082
|
+
...(0, auth_admin_tokens_procedures_js_1.createAdminTokenProcedures)({ auth, registry, network }),
|
|
958
1083
|
// ── External sign-in identities (D782) ────────────────────────
|
|
959
1084
|
//
|
|
960
1085
|
// A signed-in user links an OIDC identity to THEIR OWN account. The
|
|
@@ -966,14 +1091,36 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
966
1091
|
.input(zod_1.z.void())
|
|
967
1092
|
.output(zod_1.z.array(zod_1.z.object({ provider: zod_1.z.string(), label: zod_1.z.string() })))
|
|
968
1093
|
.query(async () => (await linkableProviders()).map(({ provider, label }) => ({ provider, label }))),
|
|
969
|
-
/**
|
|
1094
|
+
/**
|
|
1095
|
+
* Start linking `provider` to the caller; the client navigates to `url`.
|
|
1096
|
+
* `returnTo` (D801): the native app's callback — allowlisted
|
|
1097
|
+
* (`APP_AUTH_CALLBACK_URLS`); the finish then parks the link for the app
|
|
1098
|
+
* to claim with `completeExternalLink` instead of needing a session cookie.
|
|
1099
|
+
*/
|
|
970
1100
|
beginExternalLink: trpc_middleware_js_1.protectedProcedure
|
|
971
|
-
.input(zod_1.z.object({
|
|
1101
|
+
.input(zod_1.z.object({
|
|
1102
|
+
provider: zod_1.z.string().min(1),
|
|
1103
|
+
returnTo: zod_1.z.string().optional(),
|
|
1104
|
+
/** From `auth.reauthenticate` — linking needs a fresh proof, not just a session. */
|
|
1105
|
+
reauthToken: auth_reauth_procedures_js_1.ReauthTokenInput.optional(),
|
|
1106
|
+
}))
|
|
972
1107
|
.output(zod_1.z.object({ url: zod_1.z.string() }))
|
|
973
1108
|
.mutation(async ({ ctx, input }) => {
|
|
974
1109
|
if (!ctx.user)
|
|
975
1110
|
throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
|
|
976
1111
|
assertRealUserSession(ctx.user, 'Linking a sign-in account');
|
|
1112
|
+
// Per user AND per client: each call mints an entry in a bounded
|
|
1113
|
+
// shared map, so a flood must not evict other users' pending links.
|
|
1114
|
+
(0, auth_limits_js_1.limitByUser)(limiters.link, 'auth.beginExternalLink', ctx.user.id);
|
|
1115
|
+
(0, auth_limits_js_1.limitByClient)(limiters.ceremony, 'auth.beginExternalLink', ctx.req, network);
|
|
1116
|
+
(0, auth_reauth_procedures_js_1.assertReauthenticated)(auth, ctx.user, input.reauthToken);
|
|
1117
|
+
const returnTo = input.returnTo === undefined ? undefined : (0, types_1.parseAppAuthCallback)(input.returnTo);
|
|
1118
|
+
if (returnTo === null) {
|
|
1119
|
+
throw new server_1.TRPCError({
|
|
1120
|
+
code: 'BAD_REQUEST',
|
|
1121
|
+
message: 'returnTo is not an allowed app callback',
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
977
1124
|
const target = (await linkableProviders()).find((p) => p.provider === input.provider);
|
|
978
1125
|
if (target === undefined) {
|
|
979
1126
|
throw new server_1.TRPCError({
|
|
@@ -981,10 +1128,64 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
981
1128
|
message: `Sign-in provider "${input.provider}" is not available for linking`,
|
|
982
1129
|
});
|
|
983
1130
|
}
|
|
984
|
-
const handle = linkIntents.create(ctx.user.id, target.provider);
|
|
1131
|
+
const handle = linkIntents.create(ctx.user.id, target.provider, returnTo);
|
|
985
1132
|
const separator = target.startUrl.includes('?') ? '&' : '?';
|
|
986
1133
|
return { url: `${target.startUrl}${separator}link=${encodeURIComponent(handle)}` };
|
|
987
1134
|
}),
|
|
1135
|
+
/**
|
|
1136
|
+
* Claim a native link the SSO finish parked (D801). Bound HERE: the
|
|
1137
|
+
* caller must be the user the intent was minted for — a code delivered to
|
|
1138
|
+
* another account's app links nothing. The code is spent by this call
|
|
1139
|
+
* whatever the outcome.
|
|
1140
|
+
*/
|
|
1141
|
+
completeExternalLink: selfCodeProcedure
|
|
1142
|
+
.input(zod_1.z.object({ code: zod_1.z.string().min(1).max(256) }))
|
|
1143
|
+
.output(zod_1.z.object({
|
|
1144
|
+
outcome: zod_1.z.enum(['linked', 'conflict', 'expired', 'session']),
|
|
1145
|
+
provider: zod_1.z.string().optional(),
|
|
1146
|
+
}))
|
|
1147
|
+
.mutation(async ({ ctx, input }) => {
|
|
1148
|
+
if (!ctx.user)
|
|
1149
|
+
throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
|
|
1150
|
+
assertRealUserSession(ctx.user, 'Linking a sign-in account');
|
|
1151
|
+
if (nativeLink === null) {
|
|
1152
|
+
throw new server_1.TRPCError({
|
|
1153
|
+
code: 'PRECONDITION_FAILED',
|
|
1154
|
+
message: 'Native linking is not available',
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
const parked = nativeLink.completions.claim(input.code);
|
|
1158
|
+
if (parked === null)
|
|
1159
|
+
return { outcome: 'expired' };
|
|
1160
|
+
if (parked.userId !== ctx.user.id)
|
|
1161
|
+
return { outcome: 'session' };
|
|
1162
|
+
const outcome = await nativeLink.link(parked);
|
|
1163
|
+
return { outcome, provider: parked.provider };
|
|
1164
|
+
}),
|
|
1165
|
+
/**
|
|
1166
|
+
* A one-time code that opens the hub's account page signed in as the
|
|
1167
|
+
* caller, in a browser (D801) — for what only a browser can do at the
|
|
1168
|
+
* hub's origin: a passkey's WebAuthn ceremony. Single-use, 60 s, and it
|
|
1169
|
+
* lands on that page only: the route takes no destination.
|
|
1170
|
+
*/
|
|
1171
|
+
createBrowserHandoff: selfCodeProcedure
|
|
1172
|
+
.input(zod_1.z.void())
|
|
1173
|
+
.output(zod_1.z.object({ path: zod_1.z.string(), expiresAt: zod_1.z.number() }))
|
|
1174
|
+
.mutation(({ ctx }) => {
|
|
1175
|
+
if (!ctx.user)
|
|
1176
|
+
throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
|
|
1177
|
+
assertRealUserSession(ctx.user, 'Opening the account page in a browser');
|
|
1178
|
+
if (nativeLink === null) {
|
|
1179
|
+
throw new server_1.TRPCError({
|
|
1180
|
+
code: 'PRECONDITION_FAILED',
|
|
1181
|
+
message: 'Browser handoff is not available',
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const { code, expiresAt } = nativeLink.browserHandoffs.create(ctx.user.id, {
|
|
1185
|
+
purpose: 'new-session',
|
|
1186
|
+
});
|
|
1187
|
+
return { path: `${exports.BROWSER_HANDOFF_PATH}?code=${encodeURIComponent(code)}`, expiresAt };
|
|
1188
|
+
}),
|
|
988
1189
|
/** The caller's linked identities; `available: false` when the provider instance is gone or off. */
|
|
989
1190
|
listOwnExternalIdentities: trpc_middleware_js_1.protectedProcedure
|
|
990
1191
|
.input(zod_1.z.void())
|
|
@@ -1026,7 +1227,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1026
1227
|
/**
|
|
1027
1228
|
* PUBLIC — the login page's single source of pre-auth login surfaces.
|
|
1028
1229
|
* Aggregates the `login-method` collection: every auth addon (OIDC,
|
|
1029
|
-
*
|
|
1230
|
+
* WebAuthn) contributes `redirect` buttons and/or pre-auth
|
|
1030
1231
|
* `widget`s, each tagged `stage: 'primary' | 'second-factor'`. The
|
|
1031
1232
|
* login page renders `redirect` methods as generic buttons and mounts
|
|
1032
1233
|
* `widget` methods via `loadRemoteBundle` — a future SSO addon plugs
|