@camstack/server 1.2.592 → 1.2.594
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/addon-root-inventory.js +11 -50
- package/dist/agent/addon-scan.js +6 -44
- package/dist/agent/agent-addon-runtime.js +3 -8
- package/dist/agent/agent-addon-updates.js +12 -52
- package/dist/agent/agent-cap-dispatch-service.js +3 -5
- package/dist/agent/agent-config.js +7 -42
- package/dist/agent/agent-deploy-swap.js +5 -41
- package/dist/agent/agent-group-runner.js +8 -47
- package/dist/agent/agent-http-auth.js +10 -22
- package/dist/agent/agent-http.js +69 -115
- package/dist/agent/agent-hub-addon-update.js +10 -16
- package/dist/agent/agent-hub-registration.js +4 -7
- package/dist/agent/agent-infra-capabilities.js +5 -9
- package/dist/agent/agent-json-file.js +5 -42
- package/dist/agent/agent-mesh-gate.js +5 -8
- package/dist/agent/agent-node-certificates.js +25 -28
- package/dist/agent/agent-pairing-local.js +10 -47
- package/dist/agent/agent-pairing-routes.js +16 -20
- package/dist/agent/agent-pairing-store.js +18 -58
- package/dist/agent/agent-reconnect.js +1 -4
- package/dist/agent/agent-service.js +42 -78
- package/dist/agent/agent-sniffing-listener.js +11 -48
- package/dist/agent/agent-undeploy.js +5 -41
- package/dist/agent/agent-update-routes.js +14 -19
- package/dist/agent/agent-update-service.js +38 -80
- package/dist/agent/apply-model-distribution.js +1 -4
- package/dist/agent/apply-stage-dev-uploads.js +6 -9
- package/dist/agent/derive-hub-url.js +3 -8
- package/dist/agent/fetch-bundle-from-hub.js +6 -9
- package/dist/agent/hubs-found.js +6 -43
- package/dist/agent/infra-boot-guard.js +1 -4
- package/dist/agent/infra-boot-plan.js +2 -6
- package/dist/agent/main.js +158 -186
- package/dist/agent/register-agent-cap-dispatch.js +3 -6
- package/dist/agent/serial-queue.js +2 -7
- package/dist/agent-status-page.js +1 -4
- package/dist/api/addon-upload.js +22 -61
- package/dist/api/agent-addon-delivery.js +6 -11
- package/dist/api/auth-whoami.js +1 -4
- package/dist/api/backup-download-pump.js +5 -9
- package/dist/api/backup-download.js +7 -10
- package/dist/api/core/addon-settings.router.js +16 -19
- package/dist/api/core/agent-pairing.router.js +76 -79
- package/dist/api/core/agents.router.js +21 -24
- package/dist/api/core/auth-admin-tokens.procedures.js +48 -52
- package/dist/api/core/auth-device-cert.procedures.js +62 -67
- package/dist/api/core/auth-limits.js +16 -25
- package/dist/api/core/auth-reauth.procedures.js +32 -37
- package/dist/api/core/auth-revoke-credentials.procedures.js +22 -26
- package/dist/api/core/auth-sessions.procedures.js +52 -56
- package/dist/api/core/auth.router.js +300 -304
- package/dist/api/core/cap-providers.js +74 -121
- package/dist/api/core/capabilities.router.js +34 -37
- package/dist/api/core/capability-preference-writes.js +10 -17
- package/dist/api/core/certificates.router.js +106 -109
- package/dist/api/core/cluster-nodes.router.js +15 -18
- package/dist/api/core/collection-preference.js +2 -6
- package/dist/api/core/event-bus-proxy.router.js +34 -37
- package/dist/api/core/failure-contributions.js +4 -8
- package/dist/api/core/hwaccel.router.js +17 -20
- package/dist/api/core/integration-secrets.js +8 -12
- package/dist/api/core/lifecycle-job-runner.js +13 -49
- package/dist/api/core/live-events.router.js +29 -32
- package/dist/api/core/load-contributions.js +4 -8
- package/dist/api/core/logging-settings.js +30 -43
- package/dist/api/core/logs.router.js +40 -43
- package/dist/api/core/notifications.router.js +15 -18
- package/dist/api/core/public-event-filter.js +4 -8
- package/dist/api/core/repl.router.js +18 -21
- package/dist/api/core/request-census-settings.js +11 -16
- package/dist/api/core/server-management.provider.js +1 -4
- package/dist/api/core/server-name.js +9 -17
- package/dist/api/core/site-location.js +16 -24
- package/dist/api/core/stream-probe.router.js +21 -24
- package/dist/api/core/system-events.router.js +23 -26
- package/dist/api/cors/cors-policy.js +20 -29
- package/dist/api/cors/register-cors-policy.js +9 -14
- package/dist/api/deploy-stage-registry.js +6 -10
- package/dist/api/dev-uploads-bundle.js +12 -50
- package/dist/api/health/adaptive-probe.routes.js +9 -13
- package/dist/api/health/health.routes.js +8 -13
- package/dist/api/hub-cluster-url.js +3 -9
- package/dist/api/model-distributor.js +12 -50
- package/dist/api/oauth2/consent-page.js +10 -13
- package/dist/api/oauth2/oauth2-routes.js +17 -25
- package/dist/api/oauth2/private-host.js +1 -4
- package/dist/api/server-upload-propagate.js +15 -51
- package/dist/api/server-upload.js +13 -50
- package/dist/api/sse-no-compression.js +1 -4
- package/dist/api/static/frame-policy.js +4 -9
- package/dist/api/static/precompressed-asset.js +15 -57
- package/dist/api/static/spa-mount.js +8 -47
- package/dist/api/static/spa-static.js +5 -12
- package/dist/api/tarball-manifest.js +7 -44
- package/dist/api/trpc/cap-mount-helpers.js +7 -12
- package/dist/api/trpc/cap-route-error-formatter.js +10 -13
- package/dist/api/trpc/cap-router-runtime.js +40 -51
- package/dist/api/trpc/client-ip.js +9 -20
- package/dist/api/trpc/core-cap-bridge.js +32 -42
- package/dist/api/trpc/device-collection-dispatch.js +23 -27
- package/dist/api/trpc/device-config-secret-redaction.js +24 -40
- package/dist/api/trpc/generated-cap-mounts.js +219 -221
- package/dist/api/trpc/generated-cap-routers.js +4878 -4726
- package/dist/api/trpc/http-request-census-hook.js +8 -11
- package/dist/api/trpc/http-request-census.js +34 -41
- package/dist/api/trpc/http-request-census.singleton.js +4 -8
- package/dist/api/trpc/localized-schema-paths.js +1 -4
- package/dist/api/trpc/principal-visibility.js +14 -22
- package/dist/api/trpc/remote-addon-update-grant.js +1 -4
- package/dist/api/trpc/scope-access.js +10 -15
- package/dist/api/trpc/server-provided-caps.js +3 -7
- package/dist/api/trpc/share-view-access.js +6 -14
- package/dist/api/trpc/trpc-error-device-tags.js +3 -6
- package/dist/api/trpc/trpc-error-principal.js +2 -6
- package/dist/api/trpc/trpc.context.js +20 -25
- package/dist/api/trpc/trpc.middleware.js +38 -46
- package/dist/api/trpc/trpc.router.js +58 -99
- package/dist/api/trpc/user-error.js +5 -8
- package/dist/api/trpc/ws-request-census.js +17 -24
- package/dist/api/trpc/ws-slow-consumer-guard.js +11 -16
- package/dist/api/upload-auth.js +5 -9
- package/dist/auth/addon-route-jwt-gate.js +4 -7
- package/dist/auth/addon-route-share-gate.js +7 -12
- package/dist/auth/app-confirm-page.js +17 -22
- package/dist/auth/app-confirm-routes.js +26 -31
- package/dist/auth/app-confirmations.js +7 -44
- package/dist/auth/app-page-shell.js +17 -30
- package/dist/auth/app-page-strings.js +11 -17
- package/dist/auth/app-passkey-page.js +16 -21
- package/dist/auth/app-passkey-routes.js +37 -41
- package/dist/auth/app-refused-route.js +13 -18
- package/dist/auth/auth-network-policy.js +11 -15
- package/dist/auth/auth-provider-gate.js +4 -9
- package/dist/auth/auth-rate-limit.js +3 -7
- package/dist/auth/client-address.js +6 -9
- package/dist/auth/device-enroll-proofs.js +6 -43
- package/dist/auth/external-link-intents.js +3 -7
- package/dist/auth/hub-sessions.js +6 -12
- package/dist/auth/native-link-completions.js +7 -11
- package/dist/auth/native-return.js +9 -13
- package/dist/auth/passkey-provider.js +26 -32
- package/dist/auth/second-factors.js +6 -10
- package/dist/auth/session-connections.js +6 -12
- package/dist/auth/session-cookie.js +12 -23
- package/dist/auth/session-lifetimes.js +8 -12
- package/dist/auth/session-mint.js +4 -11
- package/dist/auth/sso-finish-app.js +13 -16
- package/dist/auth/sso-finish-deps.js +21 -27
- package/dist/auth/sso-finish.js +28 -38
- package/dist/auth/sso-routes.js +22 -25
- package/dist/auth/used-bridge-ids.js +1 -5
- package/dist/boot/boot-config.js +16 -55
- package/dist/boot/init-sequence.js +1 -4
- package/dist/boot/integration-id-backfill.js +3 -8
- package/dist/boot/platform-keychain-key.js +5 -42
- package/dist/boot/post-boot.service.js +14 -18
- package/dist/boot/reconcile-lifecycle-jobs.js +3 -6
- package/dist/boot/retired-public-url-env.js +4 -8
- package/dist/bootstrap-packages.js +1 -4
- package/dist/closure-node-modules.js +2 -38
- package/dist/closure-patches/patch-reach.json +6 -2
- package/dist/core/addon/addon-call-gateway.js +1 -5
- package/dist/core/addon/addon-package.service.js +83 -123
- package/dist/core/addon/addon-process-view.js +2 -6
- package/dist/core/addon/addon-realign-plan.js +1 -4
- package/dist/core/addon/addon-registry.service.js +254 -197
- package/dist/core/addon/addon-row-manifest.js +1 -4
- package/dist/core/addon/addon-search.service.js +3 -7
- package/dist/core/addon/addon-settings-provider.js +1 -3
- package/dist/core/addon/addon-uninstall.js +4 -11
- package/dist/core/addon/addon.tokens.js +2 -5
- package/dist/core/addon/device-meta-mirror.js +8 -13
- package/dist/core/addon/forked-custom-actions.js +6 -11
- package/dist/core/addon/integration-visibility.js +3 -6
- package/dist/core/addon/materialize-route-path.js +1 -4
- package/dist/core/addon/package-dir-utils.js +8 -45
- package/dist/core/addon/prune-misplaced-addons.js +5 -43
- package/dist/core/addon/runner-convergence.js +1 -4
- package/dist/core/addon/runner-serialization.js +1 -4
- package/dist/core/addon/runner-spawn-fanout.js +3 -8
- package/dist/core/addon/settings-door-wiring.js +5 -8
- package/dist/core/addon/single-flight-refresh.js +1 -4
- package/dist/core/addon-bridge/addon-bridge.service.js +7 -44
- package/dist/core/addon-pages/addon-pages.service.js +3 -40
- package/dist/core/addon-widgets/addon-widgets.service.js +4 -42
- package/dist/core/agent/agent-addon-backfill.js +9 -17
- package/dist/core/agent/agent-delivery-ledger.js +3 -7
- package/dist/core/agent/agent-liveness-event.js +2 -6
- package/dist/core/agent/agent-pairing-http.js +41 -45
- package/dist/core/agent/agent-registry.service.js +53 -92
- package/dist/core/agent/cluster-node-history-mirror.js +3 -7
- package/dist/core/agent/cluster-node-history-store.js +113 -58
- package/dist/core/agent/hub-agent-pairing.js +1 -5
- package/dist/core/agent/node-certificates-client.js +3 -6
- package/dist/core/agent/node-directory.js +1 -4
- package/dist/core/app-links/well-known-app-links.js +2 -6
- package/dist/core/auth/auth.service.js +3 -8
- package/dist/core/auth/handoff-code.service.js +11 -49
- package/dist/core/auth/record-kinds.js +35 -0
- package/dist/core/auth/share-token.service.js +174 -122
- package/dist/core/capability/capability.service.js +1 -5
- package/dist/core/config/config.schema.js +1 -6
- package/dist/core/config/config.service.js +2 -6
- package/dist/core/events/event-bus.service.js +3 -7
- package/dist/core/feature/feature.service.js +2 -6
- package/dist/core/lifecycle/lifecycle-runner.singleton.js +5 -10
- package/dist/core/lifecycle/lifecycle-state-machine.js +1 -5
- package/dist/core/logging/log-ring-buffer.js +1 -5
- package/dist/core/logging/logging.service.js +5 -9
- package/dist/core/logging/scoped-logger.js +1 -5
- package/dist/core/moleculer/agent-readiness-pull.js +1 -4
- package/dist/core/moleculer/agent-registration-request.js +3 -7
- package/dist/core/moleculer/cap-call-fn.js +39 -5
- package/dist/core/moleculer/cap-route-authority.js +3 -8
- package/dist/core/moleculer/hub-mesh-guard.js +5 -9
- package/dist/core/moleculer/moleculer.service.js +108 -119
- package/dist/core/network/network-quality.service.js +2 -6
- package/dist/core/notification/notification-wrapper.service.js +3 -7
- package/dist/core/notification/toast-wrapper.service.js +3 -7
- package/dist/core/provider/provider.tokens.js +1 -4
- package/dist/core/repl/repl-engine.service.js +4 -8
- package/dist/core/runner-health/runner-boot-check.js +7 -15
- package/dist/core/runner-health/update-preflight.js +9 -14
- package/dist/core/server-update/server-update.service.js +12 -49
- package/dist/core/server-update/system-apply-closure-patches.js +6 -42
- package/dist/core/server-update/system-ensure-prebuilds.js +3 -6
- package/dist/core/server-update/system-exec-npm.js +2 -5
- package/dist/core/storage/fs-storage-backend.js +1 -5
- package/dist/core/storage/storage-location-manager.js +1 -5
- package/dist/core/storage/storage.service.js +2 -6
- package/dist/core/streaming/ffmpeg-stream-info.js +3 -8
- package/dist/core/streaming/stream-probe.service.js +12 -16
- package/dist/core/streaming/ttl-cache.js +1 -5
- package/dist/core/topology/topology-emitter.service.js +19 -23
- package/dist/core/update-availability-emitter.js +3 -7
- package/dist/core/update-availability-store.js +8 -45
- package/dist/core/updates/agent-installed-packages.js +5 -9
- package/dist/core/updates/restart-marker.js +3 -8
- package/dist/core/updates/update-check-scheduler.js +13 -18
- package/dist/core/updates/wrapper-update-checker.js +3 -7
- package/dist/first-boot-addon-plan.js +2 -6
- package/dist/framework-nodepath.js +2 -38
- package/dist/i18n/request-lang.js +7 -11
- package/dist/launcher.js +30 -65
- package/dist/lifecycle-journal-path.js +2 -38
- package/dist/main.js +287 -324
- package/dist/manual-boot.js +120 -157
- package/dist/node-role.js +1 -4
- package/dist/package-inventory.js +7 -48
- package/dist/pending-restore.js +7 -43
- package/dist/single-copy-cleanup-runner.js +9 -45
- package/dist/single-copy-cleanup.js +7 -15
- package/dist/sqlite-restore.js +9 -48
- package/dist/starter.js +4 -39
- package/dist/ui-serving/lazy-ui-serving.js +3 -39
- package/package.json +13 -13
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Auth router — core API for login/logout/me.
|
|
4
3
|
*
|
|
@@ -14,43 +13,40 @@
|
|
|
14
13
|
* collection — the single, generic mechanism every auth addon contributes
|
|
15
14
|
* to (superseding the removed `auth.listProviders`).
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const server_name_js_1 = require("./server-name.js");
|
|
45
|
-
const types_2 = require("@camstack/types");
|
|
46
|
-
const user_error_js_1 = require("../trpc/user-error.js");
|
|
16
|
+
import { advertisedHubOrigins, DEVICE_CERT_SIGNATURE, DEVICE_SIGNATURE_MESSAGE_VERSION, DeviceCertificateLoginMethodSchema, MethodAccessSchema, parseAppAuthCallback, pickRequestOrigin, PasskeyLoginMethodSchema, PasskeySummarySchema, RedirectLoginMethodSchema, ServerNameInfoSchema, summarizeEffectiveScope, TokenScopeSchema, WidgetLoginMethodSchema, } from '@camstack/types';
|
|
17
|
+
import { TRPCError } from '@trpc/server';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { DEFAULT_AUTH_NETWORK_POLICY } from '../../auth/auth-network-policy.js';
|
|
20
|
+
import { APP_PASSKEY_PATH } from '../../auth/app-passkey-routes.js';
|
|
21
|
+
import { createRateLimiter } from '../../auth/auth-rate-limit.js';
|
|
22
|
+
import { isAuthAddonDisabled } from '../../auth/auth-provider-gate.js';
|
|
23
|
+
import { DeviceEnrollProofs } from '../../auth/device-enroll-proofs.js';
|
|
24
|
+
import { ExternalLinkIntents } from '../../auth/external-link-intents.js';
|
|
25
|
+
import { FinishRegistrationSchema, OptionsJsonSchema, resolvePasskeyProvider, SecondFactorPreferenceSchema, SetSecondFactorPreferenceSchema, } from '../../auth/passkey-provider.js';
|
|
26
|
+
import { collectSecondFactors, SecondFactorSchema, } from '../../auth/second-factors.js';
|
|
27
|
+
import { readSessionCookieFromHeader } from '../../auth/session-cookie.js';
|
|
28
|
+
import { userManagementInternal } from '../../auth/hub-sessions.js';
|
|
29
|
+
import { createSessionMinter, SessionRefreshRefusedError, SessionUserGoneError, } from '../../auth/session-mint.js';
|
|
30
|
+
import { HandoffCodeService } from '../../core/auth/handoff-code.service.js';
|
|
31
|
+
import { SHARE_TOKEN_TTL_MAX_SEC, SHARE_TOKEN_TTL_MIN_SEC, ShareTokenScopeSchema, } from '../../core/auth/share-token.service.js';
|
|
32
|
+
import { filterViewableDeviceIds } from '../trpc/principal-visibility.js';
|
|
33
|
+
import { adminProcedure, protectedProcedure, publicProcedure, trpcRouter, } from '../trpc/trpc.middleware.js';
|
|
34
|
+
import { assertAccountNotLocked, limitByClient, limitByUser, PROCESS_AUTH_LIMITERS, recordAccountFailure, } from './auth-limits.js';
|
|
35
|
+
import { createAdminTokenProcedures } from './auth-admin-tokens.procedures.js';
|
|
36
|
+
import { createRevokeCredentialsProcedures } from './auth-revoke-credentials.procedures.js';
|
|
37
|
+
import { assertReauthenticated, createReauthProcedures, ReauthTokenInput, } from './auth-reauth.procedures.js';
|
|
38
|
+
import { createDeviceCertProcedures } from './auth-device-cert.procedures.js';
|
|
39
|
+
import { createSessionProcedures } from './auth-sessions.procedures.js';
|
|
40
|
+
import { serverNameReaderFor } from './server-name.js';
|
|
41
|
+
import { userError } from '@camstack/types';
|
|
42
|
+
import { trpcUserError } from '../trpc/user-error.js';
|
|
47
43
|
/**
|
|
48
44
|
* A public procedure limited per (procedure, REAL client address) — see
|
|
49
45
|
* `auth-limits.ts` for the tiers and why the address is the forwarded client.
|
|
50
46
|
*/
|
|
51
47
|
function makeRateLimitedProcedure(limiter, network) {
|
|
52
|
-
return
|
|
53
|
-
|
|
48
|
+
return publicProcedure.use(({ ctx, path, next }) => {
|
|
49
|
+
limitByClient(limiter, path, ctx.req, network);
|
|
54
50
|
return next({ ctx });
|
|
55
51
|
});
|
|
56
52
|
}
|
|
@@ -59,7 +55,7 @@ const STEP_UP_FRESH_MS = 120_000;
|
|
|
59
55
|
/** Login methods that count as an authentication (a handoff / exchange / certificate session does not). */
|
|
60
56
|
const STEP_UP_LOGIN_METHODS = ['password', 'passkey', 'sso'];
|
|
61
57
|
/** Asks for the refresh-token flow (short access token + rotating refresh token). */
|
|
62
|
-
const RefreshableInput =
|
|
58
|
+
const RefreshableInput = z.boolean().optional();
|
|
63
59
|
/** The request's User-Agent, trimmed, for the sessions list. */
|
|
64
60
|
function clientLabel(req) {
|
|
65
61
|
const ua = req?.headers['user-agent'];
|
|
@@ -71,11 +67,11 @@ function clientLabel(req) {
|
|
|
71
67
|
* link claim, browser handoff — D801): per user, not per ip, because the
|
|
72
68
|
* caller is known and a tunnel puts every client behind one address.
|
|
73
69
|
*/
|
|
74
|
-
const SELF_CODE_LIMITER =
|
|
75
|
-
const selfCodeProcedure =
|
|
70
|
+
const SELF_CODE_LIMITER = createRateLimiter({ windowMs: 60_000, max: 10 });
|
|
71
|
+
const selfCodeProcedure = protectedProcedure.use(({ ctx, path, next }) => {
|
|
76
72
|
const verdict = SELF_CODE_LIMITER.check(`${path}:${ctx.user?.id ?? 'anonymous'}`);
|
|
77
73
|
if (!verdict.allowed) {
|
|
78
|
-
throw new
|
|
74
|
+
throw new TRPCError({
|
|
79
75
|
code: 'TOO_MANY_REQUESTS',
|
|
80
76
|
message: `Too many attempts — retry in ${verdict.retryAfterSeconds}s`,
|
|
81
77
|
});
|
|
@@ -83,7 +79,7 @@ const selfCodeProcedure = trpc_middleware_js_1.protectedProcedure.use(({ ctx, pa
|
|
|
83
79
|
return next({ ctx });
|
|
84
80
|
});
|
|
85
81
|
/** The hub route a browser-handoff code is redeemed at (`main.ts`). */
|
|
86
|
-
|
|
82
|
+
export const BROWSER_HANDOFF_PATH = '/api/auth/browser-handoff';
|
|
87
83
|
/**
|
|
88
84
|
* The available second-factor kinds a user may satisfy after the
|
|
89
85
|
* password leg. Capability-driven — `totp` maps to the
|
|
@@ -107,26 +103,26 @@ exports.BROWSER_HANDOFF_PATH = '/api/auth/browser-handoff';
|
|
|
107
103
|
* `requiresTotp` is retained (`= secondFactors.includes('totp')`) for
|
|
108
104
|
* backward compatibility with older SDK builds that branch on it.
|
|
109
105
|
*/
|
|
110
|
-
const LoginResultSchema =
|
|
111
|
-
token:
|
|
112
|
-
user:
|
|
113
|
-
id:
|
|
114
|
-
username:
|
|
115
|
-
isAdmin:
|
|
106
|
+
const LoginResultSchema = z.object({
|
|
107
|
+
token: z.string(),
|
|
108
|
+
user: z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
username: z.string(),
|
|
111
|
+
isAdmin: z.boolean(),
|
|
116
112
|
}),
|
|
117
|
-
requiresTotp:
|
|
118
|
-
secondFactors:
|
|
113
|
+
requiresTotp: z.boolean().optional(),
|
|
114
|
+
secondFactors: z.array(SecondFactorSchema).optional(),
|
|
119
115
|
/** When the access token expires (epoch ms). Absent on a challenge. */
|
|
120
|
-
expiresAt:
|
|
116
|
+
expiresAt: z.number().optional(),
|
|
121
117
|
/** The server-side login session. */
|
|
122
|
-
sessionId:
|
|
118
|
+
sessionId: z.string().optional(),
|
|
123
119
|
/** When the session expires unless refreshed (epoch ms). */
|
|
124
|
-
sessionExpiresAt:
|
|
120
|
+
sessionExpiresAt: z.number().optional(),
|
|
125
121
|
/**
|
|
126
122
|
* Only when the client asked (`refreshable: true`): swap it at
|
|
127
123
|
* `auth.refreshSession` before `expiresAt`. Store it like a password.
|
|
128
124
|
*/
|
|
129
|
-
refreshToken:
|
|
125
|
+
refreshToken: z.string().optional(),
|
|
130
126
|
});
|
|
131
127
|
// ── Login-method aggregation (login-method cap) ──────────────────────
|
|
132
128
|
//
|
|
@@ -140,8 +136,8 @@ const LoginResultSchema = zod_1.z.object({
|
|
|
140
136
|
// bundle's relative sibling imports inherit it and a CDN busts the WHOLE
|
|
141
137
|
// bundle atomically on deploy (see the stamping site below). `redirect` and
|
|
142
138
|
// `passkey` arms pass through untouched — no server-side enrichment needed.
|
|
143
|
-
const PublicWidgetLoginMethodSchema =
|
|
144
|
-
bundleUrl:
|
|
139
|
+
const PublicWidgetLoginMethodSchema = WidgetLoginMethodSchema.extend({
|
|
140
|
+
bundleUrl: z.string(),
|
|
145
141
|
});
|
|
146
142
|
/** Stable per hub PROCESS — busts the login widget bundle URL on each
|
|
147
143
|
* restart/deploy while caching cleanly within the process's uptime. */
|
|
@@ -150,24 +146,24 @@ const AUTH_BUNDLE_VERSION = String(Date.now());
|
|
|
150
146
|
// always sends `app` (`null` when there is no app path - the viewer hides the
|
|
151
147
|
// method). It stays optional in the TYPE so the admin-ui's hand-mirrored arms,
|
|
152
148
|
// which never read it, keep matching this union.
|
|
153
|
-
const PublicRedirectLoginMethodSchema =
|
|
154
|
-
const PublicPasskeyLoginMethodSchema =
|
|
155
|
-
const PublicLoginMethodSchema =
|
|
149
|
+
const PublicRedirectLoginMethodSchema = RedirectLoginMethodSchema;
|
|
150
|
+
const PublicPasskeyLoginMethodSchema = PasskeyLoginMethodSchema;
|
|
151
|
+
const PublicLoginMethodSchema = z.discriminatedUnion('kind', [
|
|
156
152
|
PublicRedirectLoginMethodSchema,
|
|
157
153
|
PublicWidgetLoginMethodSchema,
|
|
158
154
|
PublicPasskeyLoginMethodSchema,
|
|
159
|
-
|
|
155
|
+
DeviceCertificateLoginMethodSchema,
|
|
160
156
|
]);
|
|
161
157
|
/** One of the caller's linked sign-in identities, as the profile shows it (D782). */
|
|
162
|
-
const OwnExternalIdentitySchema =
|
|
163
|
-
provider:
|
|
164
|
-
subject:
|
|
165
|
-
label:
|
|
166
|
-
linkedAt:
|
|
158
|
+
const OwnExternalIdentitySchema = z.object({
|
|
159
|
+
provider: z.string(),
|
|
160
|
+
subject: z.string(),
|
|
161
|
+
label: z.string().optional(),
|
|
162
|
+
linkedAt: z.number(),
|
|
167
163
|
/** The provider instance's button label, or `null` when it is gone / disabled. */
|
|
168
|
-
providerLabel:
|
|
164
|
+
providerLabel: z.string().nullable(),
|
|
169
165
|
/** Whether the provider instance is configured and enabled right now. */
|
|
170
|
-
available:
|
|
166
|
+
available: z.boolean(),
|
|
171
167
|
});
|
|
172
168
|
// ── Share tokens (grid-view standalone share links) ──────────────────
|
|
173
169
|
//
|
|
@@ -179,26 +175,26 @@ const OwnExternalIdentitySchema = zod_1.z.object({
|
|
|
179
175
|
// TTL: a bounded number of seconds, or the EXPLICIT literal `'never'`
|
|
180
176
|
// for a permanently-shared grid (revocation is the kill switch).
|
|
181
177
|
// Omitted → 7-day default; infinite is never a silent default.
|
|
182
|
-
const CreateShareTokenInputSchema =
|
|
183
|
-
scope:
|
|
184
|
-
ttlSec:
|
|
178
|
+
const CreateShareTokenInputSchema = z.object({
|
|
179
|
+
scope: ShareTokenScopeSchema,
|
|
180
|
+
ttlSec: z
|
|
185
181
|
.union([
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
z.number().int().min(SHARE_TOKEN_TTL_MIN_SEC).max(SHARE_TOKEN_TTL_MAX_SEC),
|
|
183
|
+
z.literal('never'),
|
|
188
184
|
])
|
|
189
185
|
.optional(),
|
|
190
186
|
});
|
|
191
|
-
const ShareTokenSummarySchema =
|
|
192
|
-
id:
|
|
193
|
-
userId:
|
|
194
|
-
tokenPrefix:
|
|
195
|
-
scope:
|
|
196
|
-
createdAt:
|
|
187
|
+
const ShareTokenSummarySchema = z.object({
|
|
188
|
+
id: z.string(),
|
|
189
|
+
userId: z.string(),
|
|
190
|
+
tokenPrefix: z.string(),
|
|
191
|
+
scope: ShareTokenScopeSchema,
|
|
192
|
+
createdAt: z.number(),
|
|
197
193
|
/** Epoch ms, or `null` for never-expiring tokens. */
|
|
198
|
-
expiresAt:
|
|
194
|
+
expiresAt: z.number().nullable(),
|
|
199
195
|
/** Derived convenience flag so listings surface infinite tokens clearly. */
|
|
200
|
-
neverExpires:
|
|
201
|
-
lastUsedAt:
|
|
196
|
+
neverExpires: z.boolean(),
|
|
197
|
+
lastUsedAt: z.number().optional(),
|
|
202
198
|
});
|
|
203
199
|
function toShareTokenSummary(record) {
|
|
204
200
|
return {
|
|
@@ -222,7 +218,7 @@ function assertRealUserSession(user, what = 'Share-token management') {
|
|
|
222
218
|
// An admin token is a script's credential, never a person's session: it
|
|
223
219
|
// must not turn itself into a session (handoff codes) or mint links.
|
|
224
220
|
if (user.isScoped || user.shareView || user.adminTokenId !== undefined) {
|
|
225
|
-
throw new
|
|
221
|
+
throw new TRPCError({
|
|
226
222
|
code: 'FORBIDDEN',
|
|
227
223
|
message: `${what} requires a real user session`,
|
|
228
224
|
});
|
|
@@ -239,7 +235,7 @@ function assertRealUserSession(user, what = 'Share-token management') {
|
|
|
239
235
|
// answers with a freshly-minted session).
|
|
240
236
|
/** Custom header a cross-site form can never set — CSRF gate for the
|
|
241
237
|
* cookie-authenticated `exchangeSession` mutation. */
|
|
242
|
-
|
|
238
|
+
export const EXCHANGE_SESSION_HEADER = 'x-camstack-exchange';
|
|
243
239
|
/** Read a single-valued request header off the tRPC context request. */
|
|
244
240
|
function readRequestHeader(req, name) {
|
|
245
241
|
const value = req?.headers[name];
|
|
@@ -250,14 +246,14 @@ function readRequestHeader(req, name) {
|
|
|
250
246
|
return null;
|
|
251
247
|
}
|
|
252
248
|
/** Per-access device reach summary (counts, never id lists). */
|
|
253
|
-
const DeviceReachSummarySchema =
|
|
254
|
-
all:
|
|
255
|
-
deviceCount:
|
|
249
|
+
const DeviceReachSummarySchema = z.object({
|
|
250
|
+
all: z.boolean(),
|
|
251
|
+
deviceCount: z.number().int(),
|
|
256
252
|
});
|
|
257
253
|
/** One capability/addon grant flattened for the UI. */
|
|
258
|
-
const ScopeGrantSummarySchema =
|
|
259
|
-
name:
|
|
260
|
-
access:
|
|
254
|
+
const ScopeGrantSummarySchema = z.object({
|
|
255
|
+
name: z.string(),
|
|
256
|
+
access: z.array(MethodAccessSchema).readonly(),
|
|
261
257
|
});
|
|
262
258
|
/**
|
|
263
259
|
* The caller's RESOLVED effective scope (scope model v3). Selectors are
|
|
@@ -265,31 +261,31 @@ const ScopeGrantSummarySchema = zod_1.z.object({
|
|
|
265
261
|
* the viewer (F3/F4) can HIDE out-of-scope surfaces from a single probe. A
|
|
266
262
|
* non-admin with zero grants reports zero everything (born with no access).
|
|
267
263
|
*/
|
|
268
|
-
const EffectiveScopeSchema =
|
|
269
|
-
isAdmin:
|
|
270
|
-
allDevicesViewable:
|
|
271
|
-
viewableDeviceCount:
|
|
272
|
-
device:
|
|
264
|
+
const EffectiveScopeSchema = z.object({
|
|
265
|
+
isAdmin: z.boolean(),
|
|
266
|
+
allDevicesViewable: z.boolean(),
|
|
267
|
+
viewableDeviceCount: z.number().int(),
|
|
268
|
+
device: z.object({
|
|
273
269
|
view: DeviceReachSummarySchema,
|
|
274
270
|
create: DeviceReachSummarySchema,
|
|
275
271
|
delete: DeviceReachSummarySchema,
|
|
276
272
|
}),
|
|
277
|
-
system:
|
|
278
|
-
capabilities:
|
|
279
|
-
addons:
|
|
273
|
+
system: z.array(MethodAccessSchema).readonly(),
|
|
274
|
+
capabilities: z.array(ScopeGrantSummarySchema).readonly(),
|
|
275
|
+
addons: z.array(ScopeGrantSummarySchema).readonly(),
|
|
280
276
|
});
|
|
281
277
|
/** Wire shape of the authenticated user returned by `auth.me`. */
|
|
282
|
-
const MeSchema =
|
|
278
|
+
const MeSchema = z
|
|
283
279
|
.object({
|
|
284
|
-
id:
|
|
285
|
-
username:
|
|
286
|
-
isAdmin:
|
|
287
|
-
permissions:
|
|
288
|
-
isAdmin:
|
|
289
|
-
allowedProviders:
|
|
290
|
-
allowedDevices:
|
|
280
|
+
id: z.string(),
|
|
281
|
+
username: z.string(),
|
|
282
|
+
isAdmin: z.boolean(),
|
|
283
|
+
permissions: z.object({
|
|
284
|
+
isAdmin: z.boolean(),
|
|
285
|
+
allowedProviders: z.union([z.literal('*'), z.array(z.string())]),
|
|
286
|
+
allowedDevices: z.record(z.string(), z.unknown()),
|
|
291
287
|
}),
|
|
292
|
-
isApiKey:
|
|
288
|
+
isApiKey: z.boolean(),
|
|
293
289
|
/**
|
|
294
290
|
* The caller's resolved effective scope. Absent only on the (unreachable
|
|
295
291
|
* here — `me` is `protected`) null-principal branch.
|
|
@@ -297,9 +293,9 @@ const MeSchema = zod_1.z
|
|
|
297
293
|
scope: EffectiveScopeSchema.optional(),
|
|
298
294
|
})
|
|
299
295
|
.nullable();
|
|
300
|
-
function createAuthRouter(auth, registry, moleculer = null, shareTokens = null, handoffCodes = new
|
|
301
|
-
const network = options.network ??
|
|
302
|
-
const limiters = options.limiters ??
|
|
296
|
+
export function createAuthRouter(auth, registry, moleculer = null, shareTokens = null, handoffCodes = new HandoffCodeService(), linkIntents = new ExternalLinkIntents(), nativeLink = null, options = {}) {
|
|
297
|
+
const network = options.network ?? DEFAULT_AUTH_NETWORK_POLICY;
|
|
298
|
+
const limiters = options.limiters ?? PROCESS_AUTH_LIMITERS;
|
|
303
299
|
/** Secret-verifying legs (password / TOTP / passkey assertion / codes). */
|
|
304
300
|
const credentialProcedure = makeRateLimitedProcedure(limiters.credential, network);
|
|
305
301
|
/** Challenge-minting begin-legs — looser, they verify nothing. */
|
|
@@ -317,13 +313,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
317
313
|
* admin change made mid-flow; signing from stale claims could mint a
|
|
318
314
|
* stale-scope session.
|
|
319
315
|
*/
|
|
320
|
-
const minter =
|
|
316
|
+
const minter = createSessionMinter({
|
|
321
317
|
signer: auth,
|
|
322
318
|
directory: () => registry?.getSingleton('user-management'),
|
|
323
319
|
...(options.sessions !== undefined ? { sessions: options.sessions } : {}),
|
|
324
320
|
credentials: options.credentials ??
|
|
325
321
|
(() => {
|
|
326
|
-
const internal = registry === null ? null :
|
|
322
|
+
const internal = registry === null ? null : userManagementInternal(registry);
|
|
327
323
|
return internal === null ? null : internal.deviceCredentials;
|
|
328
324
|
}),
|
|
329
325
|
});
|
|
@@ -352,8 +348,8 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
352
348
|
return await mintSessionForUserId(userId, issue);
|
|
353
349
|
}
|
|
354
350
|
catch (error) {
|
|
355
|
-
if (error instanceof
|
|
356
|
-
throw
|
|
351
|
+
if (error instanceof SessionUserGoneError) {
|
|
352
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.user-gone', undefined, 'User no longer exists');
|
|
357
353
|
}
|
|
358
354
|
throw error;
|
|
359
355
|
}
|
|
@@ -361,11 +357,11 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
361
357
|
/** The second factors a primary login still owes (`auth/second-factors.ts`). */
|
|
362
358
|
const collectSecondFactorsFor = async (userId, primary) => {
|
|
363
359
|
const userMgmt = registry?.getSingleton('user-management');
|
|
364
|
-
return
|
|
360
|
+
return collectSecondFactors({
|
|
365
361
|
...(userMgmt && typeof userMgmt.getTotpStatus === 'function'
|
|
366
362
|
? { getTotpStatus: (input) => userMgmt.getTotpStatus(input) }
|
|
367
363
|
: {}),
|
|
368
|
-
passkeyProvider: () =>
|
|
364
|
+
passkeyProvider: () => resolvePasskeyProvider(registry, moleculer),
|
|
369
365
|
}, userId, primary);
|
|
370
366
|
};
|
|
371
367
|
/**
|
|
@@ -381,7 +377,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
381
377
|
const address = network.clientAddress(req);
|
|
382
378
|
if (!network.isTrustedNetworkLogin(address))
|
|
383
379
|
return factors;
|
|
384
|
-
const internal = registry === null ? null :
|
|
380
|
+
const internal = registry === null ? null : userManagementInternal(registry);
|
|
385
381
|
if (internal === null)
|
|
386
382
|
return factors; // cannot audit it → do not skip it
|
|
387
383
|
await internal.audit.record({
|
|
@@ -413,18 +409,18 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
413
409
|
for (const [name, value] of Object.entries(req?.headers ?? {})) {
|
|
414
410
|
headers[name.toLowerCase()] = Array.isArray(value) ? value[0] : value;
|
|
415
411
|
}
|
|
416
|
-
const picked =
|
|
412
|
+
const picked = pickRequestOrigin(headers, advertisedHubOrigins(book));
|
|
417
413
|
return (picked ??
|
|
418
414
|
book.roles.publicEndpoint.resolved ??
|
|
419
415
|
book.roles.appConnections.resolved[0] ??
|
|
420
416
|
null);
|
|
421
417
|
});
|
|
422
|
-
const enrollProofs = options.enrollProofs ?? new
|
|
418
|
+
const enrollProofs = options.enrollProofs ?? new DeviceEnrollProofs(Date.now, options.logger ?? null);
|
|
423
419
|
/** The device credential behind a session, so a session minted FROM it inherits it (D855). */
|
|
424
420
|
const credentialOfSession = (sessionId) => {
|
|
425
421
|
if (sessionId === undefined || registry === null)
|
|
426
422
|
return undefined;
|
|
427
|
-
const check =
|
|
423
|
+
const check = userManagementInternal(registry)?.sessions.check(sessionId);
|
|
428
424
|
return check?.state === 'active' ? (check.session.credentialId ?? undefined) : undefined;
|
|
429
425
|
};
|
|
430
426
|
/**
|
|
@@ -438,10 +434,10 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
438
434
|
const assertFreshAuthentication = (user, reauthToken) => {
|
|
439
435
|
const refuse = (message) => {
|
|
440
436
|
options.logger?.warn(`Step-up refused: ${message}`, { tags: { userId: user.id } });
|
|
441
|
-
throw new
|
|
437
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: `Sign in again first: ${message}` });
|
|
442
438
|
};
|
|
443
439
|
if (reauthToken !== undefined) {
|
|
444
|
-
|
|
440
|
+
assertReauthenticated(auth, user, reauthToken);
|
|
445
441
|
if (!enrollProofs.consumeReauth(reauthToken))
|
|
446
442
|
refuse('that confirmation was already used');
|
|
447
443
|
return;
|
|
@@ -449,7 +445,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
449
445
|
const sid = user.sessionId;
|
|
450
446
|
const check = sid === undefined || registry === null
|
|
451
447
|
? null
|
|
452
|
-
:
|
|
448
|
+
: userManagementInternal(registry)?.sessions.check(sid);
|
|
453
449
|
if (check?.state !== 'active')
|
|
454
450
|
return refuse('no live session');
|
|
455
451
|
if (!STEP_UP_LOGIN_METHODS.includes(check.session.method)) {
|
|
@@ -462,7 +458,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
462
458
|
return refuse('this sign-in already started a step-up');
|
|
463
459
|
}
|
|
464
460
|
};
|
|
465
|
-
const serverNameReader =
|
|
461
|
+
const serverNameReader = serverNameReaderFor(registry);
|
|
466
462
|
const aggregateLoginMethods = async () => {
|
|
467
463
|
if (!registry)
|
|
468
464
|
return [];
|
|
@@ -475,7 +471,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
475
471
|
// The operator's Enable/Disable on Identity → Authentication is the
|
|
476
472
|
// addon's ONE enable (D777). It toggles `auth-provider` /
|
|
477
473
|
// `user-passkeys`, never `login-method` — so ask the governing caps.
|
|
478
|
-
if (
|
|
474
|
+
if (isAuthAddonDisabled(registry, addonId))
|
|
479
475
|
continue;
|
|
480
476
|
let contributions;
|
|
481
477
|
try {
|
|
@@ -505,7 +501,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
505
501
|
// system browser at it; a second-factor ceremony has no app path.
|
|
506
502
|
const parsed = PublicPasskeyLoginMethodSchema.safeParse({
|
|
507
503
|
...raw,
|
|
508
|
-
app: raw.stage === 'primary' ? { kind: 'browser', startPath:
|
|
504
|
+
app: raw.stage === 'primary' ? { kind: 'browser', startPath: APP_PASSKEY_PATH } : null,
|
|
509
505
|
});
|
|
510
506
|
if (parsed.success) {
|
|
511
507
|
out.push(parsed.data);
|
|
@@ -536,7 +532,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
536
532
|
// The hub's own method (D855): a device certificate. Not an addon
|
|
537
533
|
// contribution - the device CA lives in `local-auth` - so it is added here.
|
|
538
534
|
// Offered only where the hub can actually serve it (local-auth up).
|
|
539
|
-
if (
|
|
535
|
+
if (userManagementInternal(registry) !== null)
|
|
540
536
|
out.push({
|
|
541
537
|
kind: 'device-certificate',
|
|
542
538
|
id: 'device-certificate',
|
|
@@ -547,8 +543,8 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
547
543
|
renewPath: 'auth.renewDeviceCertificate',
|
|
548
544
|
revokePath: 'auth.revokeOwnDeviceCertificate',
|
|
549
545
|
enrollSelfPath: 'auth.enrollSelf',
|
|
550
|
-
signature:
|
|
551
|
-
messageVersion:
|
|
546
|
+
signature: DEVICE_CERT_SIGNATURE,
|
|
547
|
+
messageVersion: DEVICE_SIGNATURE_MESSAGE_VERSION,
|
|
552
548
|
clients: ['native'],
|
|
553
549
|
app: { kind: 'native' },
|
|
554
550
|
});
|
|
@@ -561,9 +557,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
561
557
|
const linkableProviders = async () => (await aggregateLoginMethods()).flatMap((m) => m.kind === 'redirect' && m.linkable === true
|
|
562
558
|
? [{ provider: m.id, label: m.label, startUrl: m.startUrl }]
|
|
563
559
|
: []);
|
|
564
|
-
return
|
|
560
|
+
return trpcRouter({
|
|
565
561
|
login: credentialProcedure
|
|
566
|
-
.input(
|
|
562
|
+
.input(z.object({ username: z.string(), password: z.string(), refreshable: RefreshableInput }))
|
|
567
563
|
.output(LoginResultSchema)
|
|
568
564
|
.mutation(async ({ input, ctx }) => {
|
|
569
565
|
const userMgmt = registry?.getSingleton('user-management');
|
|
@@ -577,14 +573,14 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
577
573
|
'The `local-auth` addon failed to initialize; check server logs ' +
|
|
578
574
|
'for an error tagged `[hub/local-auth]`.');
|
|
579
575
|
}
|
|
580
|
-
|
|
576
|
+
assertAccountNotLocked(limiters, input.username);
|
|
581
577
|
const user = await userMgmt.validateCredentials({
|
|
582
578
|
username: input.username,
|
|
583
579
|
password: input.password,
|
|
584
580
|
});
|
|
585
581
|
if (!user) {
|
|
586
|
-
|
|
587
|
-
throw
|
|
582
|
+
recordAccountFailure(limiters, input.username);
|
|
583
|
+
throw userError('auth.invalid-credentials', undefined, 'Invalid credentials');
|
|
588
584
|
}
|
|
589
585
|
// ── Second-factor gate ───────────────────────────────────────
|
|
590
586
|
// After credentials validate, collect every ACTIVE second
|
|
@@ -629,7 +625,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
629
625
|
* deleted concurrently — rare).
|
|
630
626
|
*/
|
|
631
627
|
loginVerifyTotp: credentialProcedure
|
|
632
|
-
.input(
|
|
628
|
+
.input(z.object({ challengeToken: z.string(), code: z.string(), refreshable: RefreshableInput }))
|
|
633
629
|
.output(LoginResultSchema)
|
|
634
630
|
.mutation(async ({ input, ctx }) => {
|
|
635
631
|
const claims = auth.verifyTotpChallengeToken(input.challengeToken);
|
|
@@ -640,13 +636,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
640
636
|
if (!userMgmt) {
|
|
641
637
|
throw new Error('Login unavailable — `user-management` capability not registered');
|
|
642
638
|
}
|
|
643
|
-
|
|
639
|
+
assertAccountNotLocked(limiters, claims.username);
|
|
644
640
|
const verify = typeof userMgmt.verifyTotp === 'function'
|
|
645
641
|
? await userMgmt.verifyTotp({ userId: claims.userId, code: input.code.trim() })
|
|
646
642
|
: { valid: false };
|
|
647
643
|
if (!verify.valid) {
|
|
648
|
-
|
|
649
|
-
throw
|
|
644
|
+
recordAccountFailure(limiters, claims.username);
|
|
645
|
+
throw userError('auth.invalid-totp', undefined, 'Invalid TOTP code');
|
|
650
646
|
}
|
|
651
647
|
return mintSessionForUserId(claims.userId, issueFor(ctx.req, claims.method ?? 'password', input.refreshable));
|
|
652
648
|
}),
|
|
@@ -658,14 +654,14 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
658
654
|
* the client is still pre-session at this point.
|
|
659
655
|
*/
|
|
660
656
|
loginBeginPasskey: ceremonyProcedure
|
|
661
|
-
.input(
|
|
662
|
-
.output(
|
|
657
|
+
.input(z.object({ challengeToken: z.string() }))
|
|
658
|
+
.output(OptionsJsonSchema)
|
|
663
659
|
.mutation(async ({ input }) => {
|
|
664
660
|
const claims = auth.verifyTotpChallengeToken(input.challengeToken);
|
|
665
661
|
if (!claims) {
|
|
666
|
-
throw
|
|
662
|
+
throw userError('auth.login-challenge-expired', undefined, 'Invalid or expired login challenge — please re-enter your password');
|
|
667
663
|
}
|
|
668
|
-
const provider =
|
|
664
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
669
665
|
if (!provider)
|
|
670
666
|
throw new Error('Passkey authentication is not available');
|
|
671
667
|
return provider.beginAuthentication({ userId: claims.userId });
|
|
@@ -678,28 +674,28 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
678
674
|
* `loginVerifyTotp` uses (fresh user re-fetch for up-to-date scopes).
|
|
679
675
|
*/
|
|
680
676
|
loginVerifyPasskey: credentialProcedure
|
|
681
|
-
.input(
|
|
682
|
-
challengeToken:
|
|
683
|
-
response:
|
|
677
|
+
.input(z.object({
|
|
678
|
+
challengeToken: z.string(),
|
|
679
|
+
response: z.record(z.string(), z.unknown()),
|
|
684
680
|
refreshable: RefreshableInput,
|
|
685
681
|
}))
|
|
686
682
|
.output(LoginResultSchema)
|
|
687
683
|
.mutation(async ({ input, ctx }) => {
|
|
688
684
|
const claims = auth.verifyTotpChallengeToken(input.challengeToken);
|
|
689
685
|
if (!claims) {
|
|
690
|
-
throw
|
|
686
|
+
throw userError('auth.login-challenge-expired', undefined, 'Invalid or expired login challenge — please re-enter your password');
|
|
691
687
|
}
|
|
692
|
-
const provider =
|
|
688
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
693
689
|
if (!provider)
|
|
694
690
|
throw new Error('Passkey authentication is not available');
|
|
695
|
-
|
|
691
|
+
assertAccountNotLocked(limiters, claims.username);
|
|
696
692
|
const result = await provider.finishAuthentication({
|
|
697
693
|
userId: claims.userId,
|
|
698
694
|
response: input.response,
|
|
699
695
|
});
|
|
700
696
|
if (!result.verified) {
|
|
701
|
-
|
|
702
|
-
throw
|
|
697
|
+
recordAccountFailure(limiters, claims.username);
|
|
698
|
+
throw userError('auth.passkey-verification-failed', undefined, 'Passkey verification failed');
|
|
703
699
|
}
|
|
704
700
|
return mintSessionForUserId(claims.userId, issueFor(ctx.req, claims.method ?? 'password', input.refreshable));
|
|
705
701
|
}),
|
|
@@ -715,10 +711,10 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
715
711
|
* challenge store is bounded by its 5-min TTL prune.
|
|
716
712
|
*/
|
|
717
713
|
passkeyLoginBegin: ceremonyProcedure
|
|
718
|
-
.input(
|
|
719
|
-
.output(
|
|
714
|
+
.input(z.void())
|
|
715
|
+
.output(OptionsJsonSchema)
|
|
720
716
|
.mutation(async () => {
|
|
721
|
-
const provider =
|
|
717
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
722
718
|
if (!provider)
|
|
723
719
|
throw new Error('Passkey authentication is not available');
|
|
724
720
|
return provider.beginDiscoverableAuthentication({});
|
|
@@ -739,17 +735,17 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
739
735
|
* re-proving it adds nothing.
|
|
740
736
|
*/
|
|
741
737
|
passkeyLoginFinish: credentialProcedure
|
|
742
|
-
.input(
|
|
738
|
+
.input(z.object({ response: z.record(z.string(), z.unknown()), refreshable: RefreshableInput }))
|
|
743
739
|
.output(LoginResultSchema)
|
|
744
740
|
.mutation(async ({ input, ctx }) => {
|
|
745
|
-
const provider =
|
|
741
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
746
742
|
if (!provider)
|
|
747
743
|
throw new Error('Passkey authentication is not available');
|
|
748
744
|
const result = await provider.finishDiscoverableAuthentication({
|
|
749
745
|
response: input.response,
|
|
750
746
|
});
|
|
751
747
|
if (!result.verified || !result.userId) {
|
|
752
|
-
throw
|
|
748
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.passkey-verification-failed', undefined, 'Passkey verification failed');
|
|
753
749
|
}
|
|
754
750
|
const secondFactors = await factorsOwedHere(result.userId, 'passkey', ctx.req);
|
|
755
751
|
if (secondFactors.length > 0) {
|
|
@@ -794,25 +790,25 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
794
790
|
* scopes), equivalent to what `auth.login` hands out.
|
|
795
791
|
*/
|
|
796
792
|
exchangeSession: credentialProcedure
|
|
797
|
-
.input(
|
|
793
|
+
.input(z.object({ refreshable: RefreshableInput }).optional())
|
|
798
794
|
.output(LoginResultSchema)
|
|
799
795
|
.mutation(async ({ ctx, input }) => {
|
|
800
|
-
if (readRequestHeader(ctx.req,
|
|
801
|
-
throw new
|
|
796
|
+
if (readRequestHeader(ctx.req, EXCHANGE_SESSION_HEADER) !== '1') {
|
|
797
|
+
throw new TRPCError({
|
|
802
798
|
code: 'FORBIDDEN',
|
|
803
|
-
message: `Missing ${
|
|
799
|
+
message: `Missing ${EXCHANGE_SESSION_HEADER} header`,
|
|
804
800
|
});
|
|
805
801
|
}
|
|
806
|
-
const cookieToken =
|
|
802
|
+
const cookieToken = readSessionCookieFromHeader(readRequestHeader(ctx.req, 'cookie') ?? undefined);
|
|
807
803
|
if (!cookieToken) {
|
|
808
|
-
throw
|
|
804
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.no-session-cookie', undefined, 'No session cookie');
|
|
809
805
|
}
|
|
810
806
|
let payload;
|
|
811
807
|
try {
|
|
812
808
|
payload = auth.verifyToken(cookieToken);
|
|
813
809
|
}
|
|
814
810
|
catch {
|
|
815
|
-
throw
|
|
811
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.invalid-session-cookie', undefined, 'Invalid session cookie');
|
|
816
812
|
}
|
|
817
813
|
// Reject non-session JWTs that verify under the same secret:
|
|
818
814
|
// challenge/bridge tokens carry a `kind` discriminator, and a
|
|
@@ -821,7 +817,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
821
817
|
if (kind !== undefined ||
|
|
822
818
|
typeof payload.isAdmin !== 'boolean' ||
|
|
823
819
|
typeof payload.userId !== 'string') {
|
|
824
|
-
throw
|
|
820
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.invalid-session-cookie', undefined, 'Invalid session cookie');
|
|
825
821
|
}
|
|
826
822
|
// A session of its own for the client that asked (the hub-served
|
|
827
823
|
// viewer): signing out of one never signs the other out.
|
|
@@ -840,19 +836,19 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
840
836
|
/** Mint a one-time handoff code for the CALLING user. Real user
|
|
841
837
|
* sessions only — scoped (`cst_`) and share-view (`csv_`) callers
|
|
842
838
|
* must never convert themselves into a full session. */
|
|
843
|
-
createHandoffCode:
|
|
844
|
-
.input(
|
|
839
|
+
createHandoffCode: protectedProcedure
|
|
840
|
+
.input(z
|
|
845
841
|
.object({
|
|
846
|
-
purpose:
|
|
842
|
+
purpose: z.enum(['renew', 'new-session', 'step-up']).optional(),
|
|
847
843
|
/** PKCE S256 challenge of the app's verifier: redeem then requires the verifier. */
|
|
848
|
-
codeChallenge:
|
|
844
|
+
codeChallenge: z.string().min(43).max(128).optional(),
|
|
849
845
|
/** The app's anti-CSRF state, required again at redeem. */
|
|
850
|
-
state:
|
|
846
|
+
state: z.string().min(1).max(256).optional(),
|
|
851
847
|
/** `step-up`: a fresh single-use re-authentication, when the session itself is not a fresh sign-in. */
|
|
852
|
-
reauthToken:
|
|
848
|
+
reauthToken: ReauthTokenInput.optional(),
|
|
853
849
|
})
|
|
854
850
|
.optional())
|
|
855
|
-
.output(
|
|
851
|
+
.output(z.object({ code: z.string(), expiresAt: z.number() }))
|
|
856
852
|
.mutation(({ ctx, input }) => {
|
|
857
853
|
assertRealUserSession(ctx.user, 'Handoff-code minting');
|
|
858
854
|
const purpose = input?.purpose ?? 'renew';
|
|
@@ -865,7 +861,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
865
861
|
tags: { userId: ctx.user.id },
|
|
866
862
|
meta: { purpose },
|
|
867
863
|
});
|
|
868
|
-
throw new
|
|
864
|
+
throw new TRPCError({
|
|
869
865
|
code: 'BAD_REQUEST',
|
|
870
866
|
message: 'codeChallenge (S256) and state are required for this handoff',
|
|
871
867
|
});
|
|
@@ -887,12 +883,12 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
887
883
|
/** PUBLIC — redeem a one-time handoff code for a session bearer.
|
|
888
884
|
* Unknown / expired / already-used codes → UNAUTHORIZED. */
|
|
889
885
|
redeemHandoffCode: credentialProcedure
|
|
890
|
-
.input(
|
|
891
|
-
code:
|
|
886
|
+
.input(z.object({
|
|
887
|
+
code: z.string().min(1),
|
|
892
888
|
refreshable: RefreshableInput,
|
|
893
889
|
/** PKCE: required when the code was minted with a `codeChallenge`. */
|
|
894
|
-
codeVerifier:
|
|
895
|
-
state:
|
|
890
|
+
codeVerifier: z.string().min(1).max(256).optional(),
|
|
891
|
+
state: z.string().min(1).max(256).optional(),
|
|
896
892
|
}))
|
|
897
893
|
.output(LoginResultSchema)
|
|
898
894
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -914,7 +910,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
914
910
|
});
|
|
915
911
|
}
|
|
916
912
|
if (!grant || grant.purpose === 'step-up') {
|
|
917
|
-
throw
|
|
913
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.handoff-code-invalid', undefined, 'Invalid or expired handoff code');
|
|
918
914
|
}
|
|
919
915
|
// A `renew` code continues THE SESSION IT WAS MINTED FROM. One without a session would
|
|
920
916
|
// open a brand-new session with no PKCE behind it: refused.
|
|
@@ -922,7 +918,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
922
918
|
options.logger?.warn('Handoff redeem refused: a renew code with no session behind it', {
|
|
923
919
|
tags: { userId: grant.userId },
|
|
924
920
|
});
|
|
925
|
-
throw
|
|
921
|
+
throw trpcUserError('UNAUTHORIZED', 'auth.handoff-code-invalid', undefined, 'Invalid or expired handoff code');
|
|
926
922
|
}
|
|
927
923
|
if (grant.purpose === 'renew' && grant.sessionId !== undefined) {
|
|
928
924
|
// A legacy client renewing: the SAME session, its expiry slides. A
|
|
@@ -931,9 +927,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
931
927
|
return await minter.continueSession(grant.userId, grant.sessionId);
|
|
932
928
|
}
|
|
933
929
|
catch (error) {
|
|
934
|
-
if (error instanceof
|
|
935
|
-
error instanceof
|
|
936
|
-
throw new
|
|
930
|
+
if (error instanceof SessionRefreshRefusedError ||
|
|
931
|
+
error instanceof SessionUserGoneError) {
|
|
932
|
+
throw new TRPCError({
|
|
937
933
|
code: 'UNAUTHORIZED',
|
|
938
934
|
message: 'Session expired — sign in again',
|
|
939
935
|
});
|
|
@@ -947,8 +943,8 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
947
943
|
...(grant.credentialId !== undefined ? { credentialId: grant.credentialId } : {}),
|
|
948
944
|
});
|
|
949
945
|
}),
|
|
950
|
-
me:
|
|
951
|
-
.input(
|
|
946
|
+
me: protectedProcedure
|
|
947
|
+
.input(z.void())
|
|
952
948
|
.output(MeSchema)
|
|
953
949
|
.query(({ ctx }) => {
|
|
954
950
|
if (!ctx.user)
|
|
@@ -957,7 +953,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
957
953
|
// hide surfaces from a single probe (F1 #5). Admins summarise to
|
|
958
954
|
// "everything"; a scoped principal's selectors expand to counts.
|
|
959
955
|
const fleet = ctx.deviceFleet?.() ?? [];
|
|
960
|
-
const scope =
|
|
956
|
+
const scope = summarizeEffectiveScope(ctx.user.scopes ?? [], fleet, ctx.user.isAdmin);
|
|
961
957
|
return { ...ctx.user, scope };
|
|
962
958
|
}),
|
|
963
959
|
/**
|
|
@@ -981,13 +977,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
981
977
|
* and the answer is "everything", which is what the panel must show
|
|
982
978
|
* when the operator ticks the Admin box.
|
|
983
979
|
*/
|
|
984
|
-
previewScope:
|
|
985
|
-
.input(
|
|
986
|
-
scopes:
|
|
987
|
-
isAdmin:
|
|
980
|
+
previewScope: adminProcedure
|
|
981
|
+
.input(z.object({
|
|
982
|
+
scopes: z.array(TokenScopeSchema),
|
|
983
|
+
isAdmin: z.boolean().default(false),
|
|
988
984
|
}))
|
|
989
985
|
.output(EffectiveScopeSchema)
|
|
990
|
-
.query(({ ctx, input }) =>
|
|
986
|
+
.query(({ ctx, input }) => summarizeEffectiveScope(input.scopes, ctx.deviceFleet?.() ?? [], input.isAdmin)),
|
|
991
987
|
// ── Self-service profile operations ───────────────────────────────
|
|
992
988
|
//
|
|
993
989
|
// These route through the `user-management` capability provider but
|
|
@@ -1000,18 +996,18 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1000
996
|
// them directly here is fine. The trpc admin-gate on the cap router
|
|
1001
997
|
// exists only to block third-party tampering — operators acting on
|
|
1002
998
|
// themselves bypass it intentionally.
|
|
1003
|
-
changeOwnPassword:
|
|
1004
|
-
.input(
|
|
1005
|
-
currentPassword:
|
|
1006
|
-
newPassword:
|
|
999
|
+
changeOwnPassword: protectedProcedure
|
|
1000
|
+
.input(z.object({
|
|
1001
|
+
currentPassword: z.string().min(1),
|
|
1002
|
+
newPassword: z.string().min(8),
|
|
1007
1003
|
}))
|
|
1008
|
-
.output(
|
|
1004
|
+
.output(z.object({ success: z.literal(true) }))
|
|
1009
1005
|
.mutation(async ({ input, ctx }) => {
|
|
1010
1006
|
const userMgmt = registry?.getSingleton('user-management');
|
|
1011
1007
|
if (!userMgmt)
|
|
1012
1008
|
throw new Error('user-management capability not available');
|
|
1013
1009
|
if (!ctx.user)
|
|
1014
|
-
throw
|
|
1010
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1015
1011
|
// Re-validate the current password against the live store to
|
|
1016
1012
|
// confirm session identity → password ownership match. Stops a
|
|
1017
1013
|
// stolen session-token from rotating credentials silently.
|
|
@@ -1023,7 +1019,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1023
1019
|
throw new Error('Current password is incorrect');
|
|
1024
1020
|
// Hub-internal: the password changes AND every other session of the
|
|
1025
1021
|
// user ends (this one stays signed in).
|
|
1026
|
-
const internal = registry === null ? null :
|
|
1022
|
+
const internal = registry === null ? null : userManagementInternal(registry);
|
|
1027
1023
|
if (internal === null)
|
|
1028
1024
|
throw new Error('user-management capability not available');
|
|
1029
1025
|
await internal.setPassword({
|
|
@@ -1033,44 +1029,44 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1033
1029
|
});
|
|
1034
1030
|
return { success: true };
|
|
1035
1031
|
}),
|
|
1036
|
-
setupOwnTotp:
|
|
1037
|
-
.input(
|
|
1038
|
-
.output(
|
|
1032
|
+
setupOwnTotp: protectedProcedure
|
|
1033
|
+
.input(z.void())
|
|
1034
|
+
.output(z.object({ secret: z.string(), otpauthUrl: z.string() }))
|
|
1039
1035
|
.mutation(async ({ ctx }) => {
|
|
1040
1036
|
const userMgmt = registry?.getSingleton('user-management');
|
|
1041
1037
|
if (!userMgmt)
|
|
1042
1038
|
throw new Error('user-management capability not available');
|
|
1043
1039
|
if (!ctx.user)
|
|
1044
|
-
throw
|
|
1040
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1045
1041
|
return userMgmt.setupTotp({ userId: ctx.user.id });
|
|
1046
1042
|
}),
|
|
1047
|
-
confirmOwnTotp:
|
|
1048
|
-
.input(
|
|
1049
|
-
.output(
|
|
1043
|
+
confirmOwnTotp: protectedProcedure
|
|
1044
|
+
.input(z.object({ code: z.string().min(1) }))
|
|
1045
|
+
.output(z.object({ success: z.literal(true) }))
|
|
1050
1046
|
.mutation(async ({ input, ctx }) => {
|
|
1051
1047
|
const userMgmt = registry?.getSingleton('user-management');
|
|
1052
1048
|
if (!userMgmt)
|
|
1053
1049
|
throw new Error('user-management capability not available');
|
|
1054
1050
|
if (!ctx.user)
|
|
1055
|
-
throw
|
|
1051
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1056
1052
|
return userMgmt.confirmTotp({ userId: ctx.user.id, code: input.code });
|
|
1057
1053
|
}),
|
|
1058
|
-
disableOwnTotp:
|
|
1054
|
+
disableOwnTotp: protectedProcedure
|
|
1059
1055
|
// Re-authentication required: a session alone must not strip 2FA.
|
|
1060
|
-
.input(
|
|
1061
|
-
.output(
|
|
1056
|
+
.input(z.object({ reauthToken: ReauthTokenInput }).optional())
|
|
1057
|
+
.output(z.object({ success: z.literal(true) }))
|
|
1062
1058
|
.mutation(async ({ ctx, input }) => {
|
|
1063
|
-
|
|
1059
|
+
assertReauthenticated(auth, ctx.user, input?.reauthToken);
|
|
1064
1060
|
const userMgmt = registry?.getSingleton('user-management');
|
|
1065
1061
|
if (!userMgmt)
|
|
1066
1062
|
throw new Error('user-management capability not available');
|
|
1067
1063
|
if (!ctx.user)
|
|
1068
|
-
throw
|
|
1064
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1069
1065
|
return userMgmt.disableTotp({ userId: ctx.user.id });
|
|
1070
1066
|
}),
|
|
1071
|
-
getOwnTotpStatus:
|
|
1072
|
-
.input(
|
|
1073
|
-
.output(
|
|
1067
|
+
getOwnTotpStatus: protectedProcedure
|
|
1068
|
+
.input(z.void())
|
|
1069
|
+
.output(z.object({ enabled: z.boolean(), confirmedAt: z.number().nullable() }))
|
|
1074
1070
|
.query(async ({ ctx }) => {
|
|
1075
1071
|
const userMgmt = registry?.getSingleton('user-management');
|
|
1076
1072
|
if (!userMgmt || !ctx.user)
|
|
@@ -1085,24 +1081,24 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1085
1081
|
// THEIR OWN passkeys — the router is the trust boundary, exactly like
|
|
1086
1082
|
// the TOTP self-service block above. Cap-agnostic: resolved through
|
|
1087
1083
|
// the `user-passkeys` cap, never an addon id.
|
|
1088
|
-
beginOwnPasskeyRegistration:
|
|
1089
|
-
.input(
|
|
1090
|
-
.output(
|
|
1084
|
+
beginOwnPasskeyRegistration: protectedProcedure
|
|
1085
|
+
.input(z.void())
|
|
1086
|
+
.output(OptionsJsonSchema)
|
|
1091
1087
|
.mutation(async ({ ctx }) => {
|
|
1092
1088
|
if (!ctx.user)
|
|
1093
|
-
throw
|
|
1094
|
-
const provider =
|
|
1089
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1090
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
1095
1091
|
if (!provider)
|
|
1096
1092
|
throw new Error('Passkey enrollment is not available');
|
|
1097
1093
|
return provider.beginRegistration({ userId: ctx.user.id, username: ctx.user.username });
|
|
1098
1094
|
}),
|
|
1099
|
-
finishOwnPasskeyRegistration:
|
|
1100
|
-
.input(
|
|
1101
|
-
.output(
|
|
1095
|
+
finishOwnPasskeyRegistration: protectedProcedure
|
|
1096
|
+
.input(z.object({ response: z.record(z.string(), z.unknown()), label: z.string() }))
|
|
1097
|
+
.output(FinishRegistrationSchema)
|
|
1102
1098
|
.mutation(async ({ input, ctx }) => {
|
|
1103
1099
|
if (!ctx.user)
|
|
1104
|
-
throw
|
|
1105
|
-
const provider =
|
|
1100
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1101
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
1106
1102
|
if (!provider)
|
|
1107
1103
|
throw new Error('Passkey enrollment is not available');
|
|
1108
1104
|
return provider.finishRegistration({
|
|
@@ -1111,13 +1107,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1111
1107
|
label: input.label,
|
|
1112
1108
|
});
|
|
1113
1109
|
}),
|
|
1114
|
-
listOwnPasskeys:
|
|
1115
|
-
.input(
|
|
1116
|
-
.output(
|
|
1110
|
+
listOwnPasskeys: protectedProcedure
|
|
1111
|
+
.input(z.void())
|
|
1112
|
+
.output(z.array(PasskeySummarySchema))
|
|
1117
1113
|
.query(async ({ ctx }) => {
|
|
1118
1114
|
if (!ctx.user)
|
|
1119
1115
|
return [];
|
|
1120
|
-
const provider =
|
|
1116
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
1121
1117
|
if (!provider)
|
|
1122
1118
|
return [];
|
|
1123
1119
|
try {
|
|
@@ -1127,15 +1123,15 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1127
1123
|
return [];
|
|
1128
1124
|
}
|
|
1129
1125
|
}),
|
|
1130
|
-
removeOwnPasskey:
|
|
1126
|
+
removeOwnPasskey: protectedProcedure
|
|
1131
1127
|
// Re-authentication required: removing a passkey can strip a factor.
|
|
1132
|
-
.input(
|
|
1133
|
-
.output(
|
|
1128
|
+
.input(z.object({ credentialId: z.string(), reauthToken: ReauthTokenInput.optional() }))
|
|
1129
|
+
.output(z.object({ success: z.literal(true) }))
|
|
1134
1130
|
.mutation(async ({ input, ctx }) => {
|
|
1135
1131
|
if (!ctx.user)
|
|
1136
|
-
throw
|
|
1137
|
-
|
|
1138
|
-
const provider =
|
|
1132
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1133
|
+
assertReauthenticated(auth, ctx.user, input.reauthToken);
|
|
1134
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
1139
1135
|
if (!provider)
|
|
1140
1136
|
throw new Error('Passkey management is not available');
|
|
1141
1137
|
return provider.removePasskey({ userId: ctx.user.id, credentialId: input.credentialId });
|
|
@@ -1148,13 +1144,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1148
1144
|
* self-service block; reads degrade to `enabled: false` so the "My
|
|
1149
1145
|
* access" page renders even while the provider is booting.
|
|
1150
1146
|
*/
|
|
1151
|
-
getOwnPasskeySecondFactorPreference:
|
|
1152
|
-
.input(
|
|
1153
|
-
.output(
|
|
1147
|
+
getOwnPasskeySecondFactorPreference: protectedProcedure
|
|
1148
|
+
.input(z.void())
|
|
1149
|
+
.output(SecondFactorPreferenceSchema)
|
|
1154
1150
|
.query(async ({ ctx }) => {
|
|
1155
1151
|
if (!ctx.user)
|
|
1156
1152
|
return { enabled: false };
|
|
1157
|
-
const provider =
|
|
1153
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
1158
1154
|
if (!provider)
|
|
1159
1155
|
return { enabled: false };
|
|
1160
1156
|
try {
|
|
@@ -1164,13 +1160,13 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1164
1160
|
return { enabled: false };
|
|
1165
1161
|
}
|
|
1166
1162
|
}),
|
|
1167
|
-
setOwnPasskeySecondFactorPreference:
|
|
1168
|
-
.input(
|
|
1169
|
-
.output(
|
|
1163
|
+
setOwnPasskeySecondFactorPreference: protectedProcedure
|
|
1164
|
+
.input(z.object({ enabled: z.boolean() }))
|
|
1165
|
+
.output(SetSecondFactorPreferenceSchema)
|
|
1170
1166
|
.mutation(async ({ input, ctx }) => {
|
|
1171
1167
|
if (!ctx.user)
|
|
1172
|
-
throw
|
|
1173
|
-
const provider =
|
|
1168
|
+
throw userError('auth.not-authenticated', undefined, 'Not authenticated');
|
|
1169
|
+
const provider = resolvePasskeyProvider(registry, moleculer);
|
|
1174
1170
|
if (!provider)
|
|
1175
1171
|
throw new Error('Passkey management is not available');
|
|
1176
1172
|
return provider.setSecondFactorPreference({
|
|
@@ -1183,9 +1179,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1183
1179
|
// or regular); the resulting `csv_*` token authenticates as a
|
|
1184
1180
|
// restricted `share-view` principal limited to the embed surface
|
|
1185
1181
|
// + its in-scope devices (see `share-view-access.ts`). ──────────
|
|
1186
|
-
createShareToken:
|
|
1182
|
+
createShareToken: protectedProcedure
|
|
1187
1183
|
.input(CreateShareTokenInputSchema)
|
|
1188
|
-
.output(
|
|
1184
|
+
.output(z.object({ id: z.string(), token: z.string(), expiresAt: z.number().nullable() }))
|
|
1189
1185
|
.mutation(async ({ input, ctx }) => {
|
|
1190
1186
|
assertRealUserSession(ctx.user);
|
|
1191
1187
|
const service = requireShareTokens();
|
|
@@ -1202,10 +1198,10 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1202
1198
|
? input.scope
|
|
1203
1199
|
: {
|
|
1204
1200
|
...input.scope,
|
|
1205
|
-
deviceIds:
|
|
1201
|
+
deviceIds: filterViewableDeviceIds(ctx.user.scopes ?? [], input.scope.deviceIds, ctx.deviceScopeLookup),
|
|
1206
1202
|
};
|
|
1207
1203
|
if (scope.deviceIds.length === 0) {
|
|
1208
|
-
throw new
|
|
1204
|
+
throw new TRPCError({
|
|
1209
1205
|
code: 'FORBIDDEN',
|
|
1210
1206
|
message: 'None of the requested devices are within your scope — you cannot mint a share token for devices you cannot see',
|
|
1211
1207
|
});
|
|
@@ -1222,12 +1218,12 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1222
1218
|
* Home Assistant embed cache so a long-lived dashboard does not mint a
|
|
1223
1219
|
* new row every hour. Expired / missing → NOT_FOUND (caller must create).
|
|
1224
1220
|
*/
|
|
1225
|
-
extendShareToken:
|
|
1226
|
-
.input(
|
|
1227
|
-
id:
|
|
1228
|
-
ttlSec:
|
|
1221
|
+
extendShareToken: protectedProcedure
|
|
1222
|
+
.input(z.object({
|
|
1223
|
+
id: z.string().min(1),
|
|
1224
|
+
ttlSec: z.number().int().min(SHARE_TOKEN_TTL_MIN_SEC).max(SHARE_TOKEN_TTL_MAX_SEC),
|
|
1229
1225
|
}))
|
|
1230
|
-
.output(
|
|
1226
|
+
.output(z.object({ id: z.string(), expiresAt: z.number() }))
|
|
1231
1227
|
.mutation(async ({ input, ctx }) => {
|
|
1232
1228
|
assertRealUserSession(ctx.user);
|
|
1233
1229
|
const service = requireShareTokens();
|
|
@@ -1239,24 +1235,24 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1239
1235
|
callerIsAdmin: ctx.user.isAdmin,
|
|
1240
1236
|
});
|
|
1241
1237
|
if (record.expiresAt === null) {
|
|
1242
|
-
throw
|
|
1238
|
+
throw trpcUserError('BAD_REQUEST', 'auth.share-never-expiring-extend', undefined, 'A never-expiring share token cannot be extended');
|
|
1243
1239
|
}
|
|
1244
1240
|
return { id: record.id, expiresAt: record.expiresAt };
|
|
1245
1241
|
}
|
|
1246
1242
|
catch (error) {
|
|
1247
1243
|
const message = error instanceof Error ? error.message : 'Share token extend failed';
|
|
1248
1244
|
if (/not found|expired/i.test(message)) {
|
|
1249
|
-
throw new
|
|
1245
|
+
throw new TRPCError({ code: 'NOT_FOUND', message });
|
|
1250
1246
|
}
|
|
1251
1247
|
if (/owner or an admin|never-expiring/i.test(message)) {
|
|
1252
|
-
throw new
|
|
1248
|
+
throw new TRPCError({ code: 'FORBIDDEN', message });
|
|
1253
1249
|
}
|
|
1254
|
-
throw new
|
|
1250
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message });
|
|
1255
1251
|
}
|
|
1256
1252
|
}),
|
|
1257
|
-
revokeShareToken:
|
|
1258
|
-
.input(
|
|
1259
|
-
.output(
|
|
1253
|
+
revokeShareToken: protectedProcedure
|
|
1254
|
+
.input(z.object({ id: z.string().min(1) }))
|
|
1255
|
+
.output(z.object({ success: z.boolean() }))
|
|
1260
1256
|
.mutation(async ({ input, ctx }) => {
|
|
1261
1257
|
assertRealUserSession(ctx.user);
|
|
1262
1258
|
const service = requireShareTokens();
|
|
@@ -1267,7 +1263,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1267
1263
|
callerIsAdmin: ctx.user.isAdmin,
|
|
1268
1264
|
});
|
|
1269
1265
|
if (removed) {
|
|
1270
|
-
const internal = registry === null ? null :
|
|
1266
|
+
const internal = registry === null ? null : userManagementInternal(registry);
|
|
1271
1267
|
if (internal !== null) {
|
|
1272
1268
|
const ip = network.clientAddress(ctx.req).ip;
|
|
1273
1269
|
await internal.audit.record({
|
|
@@ -1287,7 +1283,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1287
1283
|
return { success: removed };
|
|
1288
1284
|
}
|
|
1289
1285
|
catch (error) {
|
|
1290
|
-
throw new
|
|
1286
|
+
throw new TRPCError({
|
|
1291
1287
|
code: 'FORBIDDEN',
|
|
1292
1288
|
message: error instanceof Error ? error.message : 'Share token revoke failed',
|
|
1293
1289
|
});
|
|
@@ -1298,15 +1294,15 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1298
1294
|
* `owner: 'all'` — every user's, admin only. Omitted keeps the original
|
|
1299
1295
|
* answer (own for a user, all for an admin) for clients that predate it.
|
|
1300
1296
|
*/
|
|
1301
|
-
listShareTokens:
|
|
1302
|
-
.input(
|
|
1303
|
-
.output(
|
|
1297
|
+
listShareTokens: protectedProcedure
|
|
1298
|
+
.input(z.object({ owner: z.enum(['self', 'all']).optional() }).optional())
|
|
1299
|
+
.output(z.array(ShareTokenSummarySchema))
|
|
1304
1300
|
.query(async ({ ctx, input }) => {
|
|
1305
1301
|
assertRealUserSession(ctx.user);
|
|
1306
1302
|
const service = requireShareTokens();
|
|
1307
1303
|
const owner = input?.owner ?? (ctx.user.isAdmin ? 'all' : 'self');
|
|
1308
1304
|
if (owner === 'all' && !ctx.user.isAdmin) {
|
|
1309
|
-
throw new
|
|
1305
|
+
throw new TRPCError({
|
|
1310
1306
|
code: 'FORBIDDEN',
|
|
1311
1307
|
message: "Listing every user's share links requires an admin",
|
|
1312
1308
|
});
|
|
@@ -1314,11 +1310,11 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1314
1310
|
const records = owner === 'all' ? await service.listAll() : await service.listForUser(ctx.user.id);
|
|
1315
1311
|
return records.map(toShareTokenSummary);
|
|
1316
1312
|
}),
|
|
1317
|
-
...
|
|
1318
|
-
...
|
|
1313
|
+
...createSessionProcedures({ registry, minter, credentialProcedure }),
|
|
1314
|
+
...createDeviceCertProcedures({
|
|
1319
1315
|
registry,
|
|
1320
1316
|
minter,
|
|
1321
|
-
publicProcedure
|
|
1317
|
+
publicProcedure,
|
|
1322
1318
|
auth,
|
|
1323
1319
|
credentialLimiter: limiters.credential,
|
|
1324
1320
|
ceremonyLimiter: limiters.ceremony,
|
|
@@ -1330,9 +1326,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1330
1326
|
resolveOrigin: resolveDeviceOrigin,
|
|
1331
1327
|
issueFor,
|
|
1332
1328
|
}),
|
|
1333
|
-
...
|
|
1334
|
-
...
|
|
1335
|
-
...
|
|
1329
|
+
...createReauthProcedures({ auth, registry, moleculer, limiters }),
|
|
1330
|
+
...createAdminTokenProcedures({ auth, registry, network }),
|
|
1331
|
+
...createRevokeCredentialsProcedures({ registry, shareTokens, network }),
|
|
1336
1332
|
// ── External sign-in identities (D782) ────────────────────────
|
|
1337
1333
|
//
|
|
1338
1334
|
// A signed-in user links an OIDC identity to THEIR OWN account. The
|
|
@@ -1340,9 +1336,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1340
1336
|
// which also requires the same user's session cookie — the user id never
|
|
1341
1337
|
// travels through the browser.
|
|
1342
1338
|
/** Sign-in providers the caller can link (redirect methods declaring `linkable`). */
|
|
1343
|
-
listLinkableProviders:
|
|
1344
|
-
.input(
|
|
1345
|
-
.output(
|
|
1339
|
+
listLinkableProviders: protectedProcedure
|
|
1340
|
+
.input(z.void())
|
|
1341
|
+
.output(z.array(z.object({ provider: z.string(), label: z.string() })))
|
|
1346
1342
|
.query(async () => (await linkableProviders()).map(({ provider, label }) => ({ provider, label }))),
|
|
1347
1343
|
/**
|
|
1348
1344
|
* Start linking `provider` to the caller; the client navigates to `url`.
|
|
@@ -1350,33 +1346,33 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1350
1346
|
* (`APP_AUTH_CALLBACK_URLS`); the finish then parks the link for the app
|
|
1351
1347
|
* to claim with `completeExternalLink` instead of needing a session cookie.
|
|
1352
1348
|
*/
|
|
1353
|
-
beginExternalLink:
|
|
1354
|
-
.input(
|
|
1355
|
-
provider:
|
|
1356
|
-
returnTo:
|
|
1349
|
+
beginExternalLink: protectedProcedure
|
|
1350
|
+
.input(z.object({
|
|
1351
|
+
provider: z.string().min(1),
|
|
1352
|
+
returnTo: z.string().optional(),
|
|
1357
1353
|
/** From `auth.reauthenticate` — linking needs a fresh proof, not just a session. */
|
|
1358
|
-
reauthToken:
|
|
1354
|
+
reauthToken: ReauthTokenInput.optional(),
|
|
1359
1355
|
}))
|
|
1360
|
-
.output(
|
|
1356
|
+
.output(z.object({ url: z.string() }))
|
|
1361
1357
|
.mutation(async ({ ctx, input }) => {
|
|
1362
1358
|
if (!ctx.user)
|
|
1363
|
-
throw new
|
|
1359
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
1364
1360
|
assertRealUserSession(ctx.user, 'Linking a sign-in account');
|
|
1365
1361
|
// Per user AND per client: each call mints an entry in a bounded
|
|
1366
1362
|
// shared map, so a flood must not evict other users' pending links.
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const returnTo = input.returnTo === undefined ? undefined :
|
|
1363
|
+
limitByUser(limiters.link, 'auth.beginExternalLink', ctx.user.id);
|
|
1364
|
+
limitByClient(limiters.ceremony, 'auth.beginExternalLink', ctx.req, network);
|
|
1365
|
+
assertReauthenticated(auth, ctx.user, input.reauthToken);
|
|
1366
|
+
const returnTo = input.returnTo === undefined ? undefined : parseAppAuthCallback(input.returnTo);
|
|
1371
1367
|
if (returnTo === null) {
|
|
1372
|
-
throw new
|
|
1368
|
+
throw new TRPCError({
|
|
1373
1369
|
code: 'BAD_REQUEST',
|
|
1374
1370
|
message: 'returnTo is not an allowed app callback',
|
|
1375
1371
|
});
|
|
1376
1372
|
}
|
|
1377
1373
|
const target = (await linkableProviders()).find((p) => p.provider === input.provider);
|
|
1378
1374
|
if (target === undefined) {
|
|
1379
|
-
throw new
|
|
1375
|
+
throw new TRPCError({
|
|
1380
1376
|
code: 'BAD_REQUEST',
|
|
1381
1377
|
message: `Sign-in provider "${input.provider}" is not available for linking`,
|
|
1382
1378
|
});
|
|
@@ -1392,17 +1388,17 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1392
1388
|
* whatever the outcome.
|
|
1393
1389
|
*/
|
|
1394
1390
|
completeExternalLink: selfCodeProcedure
|
|
1395
|
-
.input(
|
|
1396
|
-
.output(
|
|
1397
|
-
outcome:
|
|
1398
|
-
provider:
|
|
1391
|
+
.input(z.object({ code: z.string().min(1).max(256) }))
|
|
1392
|
+
.output(z.object({
|
|
1393
|
+
outcome: z.enum(['linked', 'conflict', 'expired', 'session']),
|
|
1394
|
+
provider: z.string().optional(),
|
|
1399
1395
|
}))
|
|
1400
1396
|
.mutation(async ({ ctx, input }) => {
|
|
1401
1397
|
if (!ctx.user)
|
|
1402
|
-
throw new
|
|
1398
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
1403
1399
|
assertRealUserSession(ctx.user, 'Linking a sign-in account');
|
|
1404
1400
|
if (nativeLink === null) {
|
|
1405
|
-
throw new
|
|
1401
|
+
throw new TRPCError({
|
|
1406
1402
|
code: 'PRECONDITION_FAILED',
|
|
1407
1403
|
message: 'Native linking is not available',
|
|
1408
1404
|
});
|
|
@@ -1422,14 +1418,14 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1422
1418
|
* lands on that page only: the route takes no destination.
|
|
1423
1419
|
*/
|
|
1424
1420
|
createBrowserHandoff: selfCodeProcedure
|
|
1425
|
-
.input(
|
|
1426
|
-
.output(
|
|
1421
|
+
.input(z.void())
|
|
1422
|
+
.output(z.object({ path: z.string(), expiresAt: z.number() }))
|
|
1427
1423
|
.mutation(({ ctx }) => {
|
|
1428
1424
|
if (!ctx.user)
|
|
1429
|
-
throw new
|
|
1425
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
1430
1426
|
assertRealUserSession(ctx.user, 'Opening the account page in a browser');
|
|
1431
1427
|
if (nativeLink === null) {
|
|
1432
|
-
throw new
|
|
1428
|
+
throw new TRPCError({
|
|
1433
1429
|
code: 'PRECONDITION_FAILED',
|
|
1434
1430
|
message: 'Browser handoff is not available',
|
|
1435
1431
|
});
|
|
@@ -1439,12 +1435,12 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1439
1435
|
purpose: 'new-session',
|
|
1440
1436
|
...(parentCredential !== undefined ? { credentialId: parentCredential } : {}),
|
|
1441
1437
|
});
|
|
1442
|
-
return { path: `${
|
|
1438
|
+
return { path: `${BROWSER_HANDOFF_PATH}?code=${encodeURIComponent(code)}`, expiresAt };
|
|
1443
1439
|
}),
|
|
1444
1440
|
/** The caller's linked identities; `available: false` when the provider instance is gone or off. */
|
|
1445
|
-
listOwnExternalIdentities:
|
|
1446
|
-
.input(
|
|
1447
|
-
.output(
|
|
1441
|
+
listOwnExternalIdentities: protectedProcedure
|
|
1442
|
+
.input(z.void())
|
|
1443
|
+
.output(z.array(OwnExternalIdentitySchema))
|
|
1448
1444
|
.query(async ({ ctx }) => {
|
|
1449
1445
|
if (!ctx.user)
|
|
1450
1446
|
return [];
|
|
@@ -1463,12 +1459,12 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1463
1459
|
}));
|
|
1464
1460
|
}),
|
|
1465
1461
|
/** Remove one of the caller's own links. */
|
|
1466
|
-
unlinkOwnExternalIdentity:
|
|
1467
|
-
.input(
|
|
1468
|
-
.output(
|
|
1462
|
+
unlinkOwnExternalIdentity: protectedProcedure
|
|
1463
|
+
.input(z.object({ provider: z.string().min(1), subject: z.string().min(1) }))
|
|
1464
|
+
.output(z.object({ success: z.boolean() }))
|
|
1469
1465
|
.mutation(async ({ ctx, input }) => {
|
|
1470
1466
|
if (!ctx.user)
|
|
1471
|
-
throw new
|
|
1467
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
1472
1468
|
assertRealUserSession(ctx.user, 'Unlinking a sign-in account');
|
|
1473
1469
|
const userMgmt = registry?.getSingleton('user-management');
|
|
1474
1470
|
if (!userMgmt)
|
|
@@ -1495,18 +1491,18 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1495
1491
|
* `listProviders`' old resilience) so one bad addon can't lock every
|
|
1496
1492
|
* login method out of the UI.
|
|
1497
1493
|
*/
|
|
1498
|
-
listLoginMethods:
|
|
1499
|
-
.input(
|
|
1500
|
-
.output(
|
|
1494
|
+
listLoginMethods: publicProcedure
|
|
1495
|
+
.input(z.void())
|
|
1496
|
+
.output(z.array(PublicLoginMethodSchema).readonly())
|
|
1501
1497
|
.query(async () => aggregateLoginMethods()),
|
|
1502
1498
|
/**
|
|
1503
1499
|
* The hub's human name (D861), public like `listLoginMethods` and exposing
|
|
1504
1500
|
* nothing else. A separate method on purpose: `listLoginMethods` stays the bare
|
|
1505
1501
|
* array every installed viewer already parses.
|
|
1506
1502
|
*/
|
|
1507
|
-
getServerName:
|
|
1508
|
-
.input(
|
|
1509
|
-
.output(
|
|
1503
|
+
getServerName: publicProcedure
|
|
1504
|
+
.input(z.void())
|
|
1505
|
+
.output(ServerNameInfoSchema)
|
|
1510
1506
|
.query(async () => serverNameReader.read()),
|
|
1511
1507
|
});
|
|
1512
1508
|
}
|