@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,9 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.appCancelLink = exports.appHiddenField = exports.appSubmitButton = exports.appErrorLine = exports.appButton = exports.appHint = exports.appParagraph = exports.APP_PAGE_STYLE = exports.APP_PAGE_LOGOS = void 0;
|
|
4
|
-
exports.appPageCsp = appPageCsp;
|
|
5
|
-
exports.escapeHtml = escapeHtml;
|
|
6
|
-
exports.renderAppPage = renderAppPage;
|
|
7
1
|
/**
|
|
8
2
|
* The one shell of every standalone page the hub renders for a native app
|
|
9
3
|
* sign-in (D858): the passkey page, the confirmation page, the refusal pages.
|
|
@@ -23,13 +17,13 @@ exports.renderAppPage = renderAppPage;
|
|
|
23
17
|
* of the very string that is served, computed at module load - there is no
|
|
24
18
|
* hand-pasted hash that can fall out of step with the CSS.
|
|
25
19
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { defaultTheme, themeToRootCss } from '@camstack/ui-library/theme/tokens';
|
|
28
22
|
/** The brand mark and wordmark, one per colour scheme (the files under `addon-admin-ui/public/brand`). */
|
|
29
23
|
const LOGO_DARK = '<svg class="logo logo-dark" role="img" aria-label="CamStack" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 48" fill="none"><g transform="translate(4, 2) scale(0.68)"><rect x="8" y="28" width="48" height="28" rx="6" fill="#292524"/><rect x="12" y="16" width="40" height="28" rx="6" fill="#44403c"/><rect x="16" y="4" width="32" height="28" rx="6" fill="#f59e42"/><circle cx="32" cy="18" r="8" fill="#0c0a09" opacity="0.3"/><circle cx="32" cy="18" r="5" fill="#0c0a09" opacity="0.2"/><circle cx="32" cy="18" r="2.5" fill="#fafaf9" opacity="0.8"/></g><text x="52" y="31" font-family="system-ui, -apple-system, sans-serif" font-size="26" font-weight="700" letter-spacing="-0.5" fill="#fafaf9">Cam<tspan fill="#f59e42">Stack</tspan></text></svg>';
|
|
30
24
|
const LOGO_LIGHT = '<svg class="logo logo-light" role="img" aria-label="CamStack" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 48" fill="none"><g transform="translate(4, 2) scale(0.68)"><rect x="8" y="28" width="48" height="28" rx="6" fill="#d6d3d1"/><rect x="12" y="16" width="40" height="28" rx="6" fill="#a8a29e"/><rect x="16" y="4" width="32" height="28" rx="6" fill="#e67e22"/><circle cx="32" cy="18" r="8" fill="#fafaf9" opacity="0.3"/><circle cx="32" cy="18" r="5" fill="#fafaf9" opacity="0.2"/><circle cx="32" cy="18" r="2.5" fill="#1c1917" opacity="0.7"/></g><text x="52" y="31" font-family="system-ui, -apple-system, sans-serif" font-size="26" font-weight="700" letter-spacing="-0.5" fill="#1c1917">Cam<tspan fill="#e67e22">Stack</tspan></text></svg>';
|
|
31
25
|
/** The logo markup, exposed so a spec can hold it against the brand files. */
|
|
32
|
-
|
|
26
|
+
export const APP_PAGE_LOGOS = {
|
|
33
27
|
dark: LOGO_DARK,
|
|
34
28
|
light: LOGO_LIGHT,
|
|
35
29
|
};
|
|
@@ -68,19 +62,19 @@ const COMPONENT_CSS = [
|
|
|
68
62
|
`.actions{display:flex;flex-direction:column;gap:var(--spacing-md)}`,
|
|
69
63
|
].join('');
|
|
70
64
|
/** The one stylesheet, as served: the generated tokens followed by the component rules. */
|
|
71
|
-
|
|
72
|
-
const sha256Source = (text) => `'sha256-${
|
|
65
|
+
export const APP_PAGE_STYLE = `${themeToRootCss(defaultTheme)}${COMPONENT_CSS}`;
|
|
66
|
+
const sha256Source = (text) => `'sha256-${createHash('sha256').update(text).digest('base64')}'`;
|
|
73
67
|
/**
|
|
74
68
|
* The CSP for a page rendered by {@link renderAppPage}: the inline style and
|
|
75
69
|
* (when there is one) the inline script by hash, same-origin requests only, no
|
|
76
70
|
* framing, no base rewrite. The hashes are computed from the strings the page
|
|
77
71
|
* is built from, so they cannot disagree with it.
|
|
78
72
|
*/
|
|
79
|
-
function appPageCsp(options = {}) {
|
|
73
|
+
export function appPageCsp(options = {}) {
|
|
80
74
|
return [
|
|
81
75
|
"default-src 'none'",
|
|
82
76
|
...(options.script === undefined ? [] : [`script-src ${sha256Source(options.script)}`]),
|
|
83
|
-
`style-src ${sha256Source(
|
|
77
|
+
`style-src ${sha256Source(APP_PAGE_STYLE)}`,
|
|
84
78
|
...(options.script === undefined ? [] : ["connect-src 'self'"]),
|
|
85
79
|
"base-uri 'none'",
|
|
86
80
|
`form-action ${options.formToSelf === true ? "'self'" : "'none'"}`,
|
|
@@ -88,36 +82,29 @@ function appPageCsp(options = {}) {
|
|
|
88
82
|
].join('; ');
|
|
89
83
|
}
|
|
90
84
|
/** Escapes text for an HTML text node or a double-quoted attribute. */
|
|
91
|
-
function escapeHtml(text) {
|
|
85
|
+
export function escapeHtml(text) {
|
|
92
86
|
return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
93
87
|
}
|
|
94
88
|
/** A full page: doctype, viewport (with safe-area fit), tokens, logo, a card holding `body`. */
|
|
95
|
-
function renderAppPage(input) {
|
|
89
|
+
export function renderAppPage(input) {
|
|
96
90
|
const referrer = input.noReferrer === true ? '<meta name="referrer" content="no-referrer">' : '';
|
|
97
91
|
const script = input.script === undefined ? '' : `<script>${input.script}</script>`;
|
|
98
92
|
const strings = Object.entries(input.scriptStrings ?? {})
|
|
99
93
|
.map(([key, value]) => ` data-s-${key}="${escapeHtml(value)}"`)
|
|
100
94
|
.join('');
|
|
101
|
-
return `<!doctype html><html lang="${input.lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="light dark">${referrer}<title>${escapeHtml(input.title)}</title><style>${
|
|
95
|
+
return `<!doctype html><html lang="${input.lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="light dark">${referrer}<title>${escapeHtml(input.title)}</title><style>${APP_PAGE_STYLE}</style></head><body${strings}><main><div class="brand">${LOGO_DARK}${LOGO_LIGHT}</div><div class="card"><h1>${escapeHtml(input.heading)}</h1>${input.body}</div></main>${script}</body></html>`;
|
|
102
96
|
}
|
|
103
97
|
/** A paragraph of the card (`text` is escaped). */
|
|
104
|
-
const appParagraph = (text) => `<p>${escapeHtml(text)}</p>`;
|
|
105
|
-
exports.appParagraph = appParagraph;
|
|
98
|
+
export const appParagraph = (text) => `<p>${escapeHtml(text)}</p>`;
|
|
106
99
|
/** A smaller, muted paragraph (`text` is escaped). */
|
|
107
|
-
const appHint = (text) => `<p class="hint">${escapeHtml(text)}</p>`;
|
|
108
|
-
exports.appHint = appHint;
|
|
100
|
+
export const appHint = (text) => `<p class="hint">${escapeHtml(text)}</p>`;
|
|
109
101
|
/** The primary button; `attributes` is trusted markup (already escaped), `label` is escaped. */
|
|
110
|
-
const appButton = (label, attributes) => `<button class="btn" type="button" ${attributes}>${escapeHtml(label)}</button>`;
|
|
111
|
-
exports.appButton = appButton;
|
|
102
|
+
export const appButton = (label, attributes) => `<button class="btn" type="button" ${attributes}>${escapeHtml(label)}</button>`;
|
|
112
103
|
/** The error line a script fills in; empty it renders nothing. */
|
|
113
|
-
const appErrorLine = (id) => `<p class="error" id="${id}" role="alert"></p>`;
|
|
114
|
-
exports.appErrorLine = appErrorLine;
|
|
104
|
+
export const appErrorLine = (id) => `<p class="error" id="${id}" role="alert"></p>`;
|
|
115
105
|
/** A submit button of a form (`name`/`value` are escaped); `secondary` is the outlined, non-default choice. */
|
|
116
|
-
const appSubmitButton = (label, name, value, secondary = false) => `<button class="${secondary ? 'btn secondary' : 'btn'}" type="submit" name="${escapeHtml(name)}" value="${escapeHtml(value)}">${escapeHtml(label)}</button>`;
|
|
117
|
-
exports.appSubmitButton = appSubmitButton;
|
|
106
|
+
export const appSubmitButton = (label, name, value, secondary = false) => `<button class="${secondary ? 'btn secondary' : 'btn'}" type="submit" name="${escapeHtml(name)}" value="${escapeHtml(value)}">${escapeHtml(label)}</button>`;
|
|
118
107
|
/** A hidden form field (`name`/`value` are escaped). */
|
|
119
|
-
const appHiddenField = (name, value) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`;
|
|
120
|
-
exports.appHiddenField = appHiddenField;
|
|
108
|
+
export const appHiddenField = (name, value) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`;
|
|
121
109
|
/** The quiet link under the actions (Cancel); `href` is escaped here. */
|
|
122
|
-
const appCancelLink = (href, label) => `<a class="link" href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
|
|
123
|
-
exports.appCancelLink = appCancelLink;
|
|
110
|
+
export const appCancelLink = (href, label) => `<a class="link" href="${escapeHtml(href)}">${escapeHtml(label)}</a>`;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.APP_PAGE_STRINGS = exports.APP_PAGE_DEFAULT_LANG = void 0;
|
|
4
|
-
exports.appStrings = appStrings;
|
|
5
|
-
exports.langFromAcceptLanguage = langFromAcceptLanguage;
|
|
6
|
-
exports.resolveAppPageLang = resolveAppPageLang;
|
|
7
1
|
/**
|
|
8
2
|
* Every word the hub's app landing pages show (D858), in both languages, and
|
|
9
3
|
* the one rule that picks the language.
|
|
@@ -18,8 +12,8 @@ exports.resolveAppPageLang = resolveAppPageLang;
|
|
|
18
12
|
* (allow-listed) wins; else the first supported tag of `Accept-Language`
|
|
19
13
|
* (`it*` -> `it`); else English. Neither input is ever reflected into a page.
|
|
20
14
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
import { APP_START_REFUSAL_MESSAGES, parseAppPageLang, } from '@camstack/types';
|
|
16
|
+
export const APP_PAGE_DEFAULT_LANG = 'en';
|
|
23
17
|
const EN = {
|
|
24
18
|
confirm: {
|
|
25
19
|
title: 'Sign in the CamStack app',
|
|
@@ -48,7 +42,7 @@ const EN = {
|
|
|
48
42
|
title: 'Sign-in request refused',
|
|
49
43
|
hint: 'Close this window and try again from the app.',
|
|
50
44
|
generic: 'The sign-in request was refused.',
|
|
51
|
-
reasons:
|
|
45
|
+
reasons: APP_START_REFUSAL_MESSAGES,
|
|
52
46
|
},
|
|
53
47
|
consent: {
|
|
54
48
|
titlePrefix: 'CamStack · Authorize ',
|
|
@@ -121,12 +115,12 @@ const IT = {
|
|
|
121
115
|
signInFailed: 'Accesso non riuscito. Riprova.',
|
|
122
116
|
},
|
|
123
117
|
};
|
|
124
|
-
|
|
125
|
-
function appStrings(lang) {
|
|
126
|
-
return
|
|
118
|
+
export const APP_PAGE_STRINGS = { en: EN, it: IT };
|
|
119
|
+
export function appStrings(lang) {
|
|
120
|
+
return APP_PAGE_STRINGS[lang];
|
|
127
121
|
}
|
|
128
122
|
/** The first supported language of an `Accept-Language` value (by q, then order), or `undefined`. */
|
|
129
|
-
function langFromAcceptLanguage(header) {
|
|
123
|
+
export function langFromAcceptLanguage(header) {
|
|
130
124
|
const value = typeof header === 'string' ? header : header?.join(',');
|
|
131
125
|
if (value === undefined || value.length > 512)
|
|
132
126
|
return undefined;
|
|
@@ -143,15 +137,15 @@ function langFromAcceptLanguage(header) {
|
|
|
143
137
|
.filter((t) => Number.isFinite(t.weight) && t.weight > 0)
|
|
144
138
|
.sort((a, b) => b.weight - a.weight || a.index - b.index);
|
|
145
139
|
for (const { primary } of ranked) {
|
|
146
|
-
const lang =
|
|
140
|
+
const lang = parseAppPageLang(primary);
|
|
147
141
|
if (lang !== undefined)
|
|
148
142
|
return lang;
|
|
149
143
|
}
|
|
150
144
|
return undefined;
|
|
151
145
|
}
|
|
152
146
|
/** The page language: the app's `lang` (allow-listed), else `Accept-Language`, else English. */
|
|
153
|
-
function resolveAppPageLang(input) {
|
|
154
|
-
return (
|
|
147
|
+
export function resolveAppPageLang(input) {
|
|
148
|
+
return (parseAppPageLang(input.lang) ??
|
|
155
149
|
langFromAcceptLanguage(input.acceptLanguage) ??
|
|
156
|
-
|
|
150
|
+
APP_PAGE_DEFAULT_LANG);
|
|
157
151
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.APP_PASSKEY_REFUSAL_CSP = exports.APP_PASSKEY_CSP = void 0;
|
|
4
|
-
exports.appPasskeyPage = appPasskeyPage;
|
|
5
|
-
exports.appPasskeyRefusalPage = appPasskeyRefusalPage;
|
|
6
1
|
/**
|
|
7
2
|
* The standalone passkey page for a native app sign-in (D858).
|
|
8
3
|
*
|
|
@@ -19,8 +14,8 @@ exports.appPasskeyRefusalPage = appPasskeyRefusalPage;
|
|
|
19
14
|
* specific is ever placed in the script; the only server-rendered value is the
|
|
20
15
|
* (allow-listed, HTML-escaped) cancel link.
|
|
21
16
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
import { appButton, appCancelLink, appErrorLine, appHint, appPageCsp, appParagraph, escapeHtml, renderAppPage, } from './app-page-shell.js';
|
|
18
|
+
import { appStrings } from './app-page-strings.js';
|
|
24
19
|
const PAGE_SCRIPT = `(function () {
|
|
25
20
|
var q = new URLSearchParams(location.search);
|
|
26
21
|
var base = location.pathname.replace(/\\/+$/, '');
|
|
@@ -117,11 +112,11 @@ const PAGE_SCRIPT = `(function () {
|
|
|
117
112
|
* The page's CSP: the shell's style and the one script by hash, same-origin
|
|
118
113
|
* requests only, no framing, no base rewrite, no form posts, no plugins.
|
|
119
114
|
*/
|
|
120
|
-
|
|
115
|
+
export const APP_PASSKEY_CSP = appPageCsp({ script: PAGE_SCRIPT });
|
|
121
116
|
/** The page; `cancelHref` is the app callback carrying `error=cancelled`. */
|
|
122
|
-
function appPasskeyPage(cancelHref, lang) {
|
|
123
|
-
const t =
|
|
124
|
-
return
|
|
117
|
+
export function appPasskeyPage(cancelHref, lang) {
|
|
118
|
+
const t = appStrings(lang);
|
|
119
|
+
return renderAppPage({
|
|
125
120
|
lang,
|
|
126
121
|
title: t.passkey.title,
|
|
127
122
|
heading: t.passkey.heading,
|
|
@@ -134,23 +129,23 @@ function appPasskeyPage(cancelHref, lang) {
|
|
|
134
129
|
'sign-in-failed': t.script.signInFailed,
|
|
135
130
|
},
|
|
136
131
|
body: [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`<noscript><p class="error">${
|
|
132
|
+
appParagraph(t.passkey.body),
|
|
133
|
+
appButton(t.passkey.button, 'id="go"'),
|
|
134
|
+
appErrorLine('msg'),
|
|
135
|
+
appCancelLink(cancelHref, t.passkey.cancel),
|
|
136
|
+
`<noscript><p class="error">${escapeHtml(t.passkey.noscript)}</p></noscript>`,
|
|
142
137
|
].join(''),
|
|
143
138
|
});
|
|
144
139
|
}
|
|
145
140
|
/** The page for a request the hub refuses to serve: a sentence, no script, no assets. */
|
|
146
|
-
function appPasskeyRefusalPage(message, lang) {
|
|
147
|
-
const t =
|
|
148
|
-
return
|
|
141
|
+
export function appPasskeyRefusalPage(message, lang) {
|
|
142
|
+
const t = appStrings(lang).refused;
|
|
143
|
+
return renderAppPage({
|
|
149
144
|
lang,
|
|
150
145
|
title: t.title,
|
|
151
146
|
heading: t.title,
|
|
152
|
-
body:
|
|
147
|
+
body: appParagraph(message) + appHint(t.hint),
|
|
153
148
|
});
|
|
154
149
|
}
|
|
155
150
|
/** CSP of the refusal page: the shell's style only, nothing runs. */
|
|
156
|
-
|
|
151
|
+
export const APP_PASSKEY_REFUSAL_CSP = appPageCsp();
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.APP_PASSKEY_PATH = void 0;
|
|
4
|
-
exports.registerAppPasskeyRoutes = registerAppPasskeyRoutes;
|
|
5
1
|
/**
|
|
6
2
|
* The passkey app sign-in routes (D858):
|
|
7
3
|
*
|
|
@@ -18,30 +14,30 @@ exports.registerAppPasskeyRoutes = registerAppPasskeyRoutes;
|
|
|
18
14
|
* `{ error }` for a request that is not an app flow at all), so the page just
|
|
19
15
|
* navigates. The app's parameters are never logged.
|
|
20
16
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const FinishBodySchema =
|
|
30
|
-
response:
|
|
31
|
-
code_challenge:
|
|
32
|
-
code_challenge_method:
|
|
33
|
-
state:
|
|
34
|
-
purpose:
|
|
35
|
-
redirect:
|
|
36
|
-
lang:
|
|
17
|
+
import { APP_FLOW_REFUSAL_MESSAGES, appFlowLocation, parseAppFlowRequest, } from '@camstack/types';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { APP_CONFIRM_PARAM, appConfirmCookie } from './app-confirm-routes.js';
|
|
20
|
+
import { APP_PASSKEY_CSP, APP_PASSKEY_REFUSAL_CSP, appPasskeyPage, appPasskeyRefusalPage, } from './app-passkey-page.js';
|
|
21
|
+
import { appStrings, resolveAppPageLang } from './app-page-strings.js';
|
|
22
|
+
import { clientKeyFromRequest } from './auth-rate-limit.js';
|
|
23
|
+
import { SessionUserGoneError } from './session-mint.js';
|
|
24
|
+
export const APP_PASSKEY_PATH = '/auth/app/passkey';
|
|
25
|
+
const FinishBodySchema = z.object({
|
|
26
|
+
response: z.record(z.string(), z.unknown()),
|
|
27
|
+
code_challenge: z.string().optional(),
|
|
28
|
+
code_challenge_method: z.string().optional(),
|
|
29
|
+
state: z.string().optional(),
|
|
30
|
+
purpose: z.string().optional(),
|
|
31
|
+
redirect: z.string().optional(),
|
|
32
|
+
lang: z.string().optional(),
|
|
37
33
|
});
|
|
38
34
|
function noStore(reply) {
|
|
39
35
|
reply.header('Cache-Control', 'no-store');
|
|
40
36
|
reply.header('Referrer-Policy', 'no-referrer');
|
|
41
37
|
}
|
|
42
|
-
function registerAppPasskeyRoutes(fastify, deps) {
|
|
38
|
+
export function registerAppPasskeyRoutes(fastify, deps) {
|
|
43
39
|
const limited = (bucket, request, reply) => {
|
|
44
|
-
const client =
|
|
40
|
+
const client = clientKeyFromRequest(request, deps.network);
|
|
45
41
|
if (client === null)
|
|
46
42
|
return false;
|
|
47
43
|
const verdict = deps.limiter.check(`${bucket}:${client}`);
|
|
@@ -51,11 +47,11 @@ function registerAppPasskeyRoutes(fastify, deps) {
|
|
|
51
47
|
void reply.status(429).send({ error: 'Too many attempts' });
|
|
52
48
|
return true;
|
|
53
49
|
};
|
|
54
|
-
fastify.get(
|
|
50
|
+
fastify.get(APP_PASSKEY_PATH, async (request, reply) => {
|
|
55
51
|
noStore(reply);
|
|
56
52
|
reply.type('text/html; charset=utf-8');
|
|
57
|
-
const parsed =
|
|
58
|
-
const lang =
|
|
53
|
+
const parsed = parseAppFlowRequest(request.query);
|
|
54
|
+
const lang = resolveAppPageLang({
|
|
59
55
|
lang: parsed.ok ? parsed.request.lang : request.query['lang'],
|
|
60
56
|
acceptLanguage: request.headers['accept-language'],
|
|
61
57
|
});
|
|
@@ -63,26 +59,26 @@ function registerAppPasskeyRoutes(fastify, deps) {
|
|
|
63
59
|
deps.logger.warn('Passkey app sign-in refused at the page', {
|
|
64
60
|
meta: { reason: parsed.reason },
|
|
65
61
|
});
|
|
66
|
-
reply.header('Content-Security-Policy',
|
|
62
|
+
reply.header('Content-Security-Policy', APP_PASSKEY_REFUSAL_CSP);
|
|
67
63
|
return reply
|
|
68
64
|
.status(400)
|
|
69
|
-
.send(
|
|
65
|
+
.send(appPasskeyRefusalPage(appStrings(lang).refused.reasons[parsed.reason], lang));
|
|
70
66
|
}
|
|
71
|
-
reply.header('Content-Security-Policy',
|
|
72
|
-
const cancel =
|
|
73
|
-
return reply.send(
|
|
67
|
+
reply.header('Content-Security-Policy', APP_PASSKEY_CSP);
|
|
68
|
+
const cancel = appFlowLocation(parsed.request.redirect, { error: 'cancelled' }, parsed.request.state);
|
|
69
|
+
return reply.send(appPasskeyPage(cancel, lang));
|
|
74
70
|
});
|
|
75
|
-
fastify.post(`${
|
|
71
|
+
fastify.post(`${APP_PASSKEY_PATH}/begin`, async (request, reply) => {
|
|
76
72
|
noStore(reply);
|
|
77
73
|
if (limited('app-passkey-begin', request, reply))
|
|
78
74
|
return reply;
|
|
79
|
-
const body =
|
|
75
|
+
const body = z.record(z.string(), z.unknown()).safeParse(request.body);
|
|
80
76
|
const query = body.success
|
|
81
77
|
? Object.fromEntries(Object.entries(body.data).flatMap(([k, v]) => (typeof v === 'string' ? [[k, v]] : [])))
|
|
82
78
|
: {};
|
|
83
|
-
const parsed =
|
|
79
|
+
const parsed = parseAppFlowRequest(query);
|
|
84
80
|
if (!parsed.ok)
|
|
85
|
-
return reply.status(400).send({ error:
|
|
81
|
+
return reply.status(400).send({ error: APP_FLOW_REFUSAL_MESSAGES[parsed.reason] });
|
|
86
82
|
const provider = deps.passkeys();
|
|
87
83
|
if (provider === null)
|
|
88
84
|
return reply.status(503).send({ error: 'Passkeys are not available.' });
|
|
@@ -96,14 +92,14 @@ function registerAppPasskeyRoutes(fastify, deps) {
|
|
|
96
92
|
return reply.status(500).send({ error: 'Passkeys are not available.' });
|
|
97
93
|
}
|
|
98
94
|
});
|
|
99
|
-
fastify.post(`${
|
|
95
|
+
fastify.post(`${APP_PASSKEY_PATH}/finish`, async (request, reply) => {
|
|
100
96
|
noStore(reply);
|
|
101
97
|
if (limited('app-passkey-finish', request, reply))
|
|
102
98
|
return reply;
|
|
103
99
|
const body = FinishBodySchema.safeParse(request.body);
|
|
104
100
|
if (!body.success)
|
|
105
101
|
return reply.status(400).send({ error: 'Malformed sign-in request.' });
|
|
106
|
-
const parsed =
|
|
102
|
+
const parsed = parseAppFlowRequest({
|
|
107
103
|
code_challenge: body.data.code_challenge,
|
|
108
104
|
code_challenge_method: body.data.code_challenge_method,
|
|
109
105
|
state: body.data.state,
|
|
@@ -112,11 +108,11 @@ function registerAppPasskeyRoutes(fastify, deps) {
|
|
|
112
108
|
lang: body.data.lang,
|
|
113
109
|
});
|
|
114
110
|
if (!parsed.ok)
|
|
115
|
-
return reply.status(400).send({ error:
|
|
111
|
+
return reply.status(400).send({ error: APP_FLOW_REFUSAL_MESSAGES[parsed.reason] });
|
|
116
112
|
const app = parsed.request;
|
|
117
113
|
const deny = (error) => {
|
|
118
114
|
deps.logger.warn('Passkey app sign-in refused', { meta: { error } });
|
|
119
|
-
return reply.send({ location:
|
|
115
|
+
return reply.send({ location: appFlowLocation(app.redirect, { error }, app.state) });
|
|
120
116
|
};
|
|
121
117
|
const provider = deps.passkeys();
|
|
122
118
|
if (provider === null)
|
|
@@ -142,13 +138,13 @@ function registerAppPasskeyRoutes(fastify, deps) {
|
|
|
142
138
|
app,
|
|
143
139
|
authMethod: 'passkey',
|
|
144
140
|
});
|
|
145
|
-
reply.header('Set-Cookie',
|
|
141
|
+
reply.header('Set-Cookie', appConfirmCookie(parked.cookie, request.protocol === 'https'));
|
|
146
142
|
return reply.send({
|
|
147
|
-
confirm: `confirm?${
|
|
143
|
+
confirm: `confirm?${APP_CONFIRM_PARAM}=${encodeURIComponent(parked.token)}`,
|
|
148
144
|
});
|
|
149
145
|
}
|
|
150
146
|
catch (error) {
|
|
151
|
-
if (error instanceof
|
|
147
|
+
if (error instanceof SessionUserGoneError)
|
|
152
148
|
return deny('user-gone');
|
|
153
149
|
deps.logger.error('Passkey app sign-in failed', {
|
|
154
150
|
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.APP_REFUSED_CSP = void 0;
|
|
4
|
-
exports.appRefusedPage = appRefusedPage;
|
|
5
|
-
exports.registerAppRefusedRoute = registerAppRefusedRoute;
|
|
6
1
|
/**
|
|
7
2
|
* `GET /auth/app/refused?reason=<code>` - the page that says why a native app
|
|
8
3
|
* sign-in could not start (D858).
|
|
@@ -12,29 +7,29 @@ exports.registerAppRefusedRoute = registerAppRefusedRoute;
|
|
|
12
7
|
* sentence, in the language of `lang`/`Accept-Language`, in the same shell as every other app page. The query is never echoed: an unknown or
|
|
13
8
|
* absent code gets a generic sentence, a known one its fixed words.
|
|
14
9
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
import { APP_REFUSED_PATH, isAppStartRefusal } from '@camstack/types';
|
|
11
|
+
import { appHint, appPageCsp, appParagraph, renderAppPage } from './app-page-shell.js';
|
|
12
|
+
import { appStrings, resolveAppPageLang } from './app-page-strings.js';
|
|
18
13
|
/** The refusal page for `reason` (a known code, or anything: unknown renders the generic sentence). */
|
|
19
|
-
function appRefusedPage(reason, lang) {
|
|
20
|
-
const t =
|
|
21
|
-
const message =
|
|
22
|
-
return
|
|
14
|
+
export function appRefusedPage(reason, lang) {
|
|
15
|
+
const t = appStrings(lang).refused;
|
|
16
|
+
const message = isAppStartRefusal(reason) ? t.reasons[reason] : t.generic;
|
|
17
|
+
return renderAppPage({
|
|
23
18
|
lang,
|
|
24
19
|
title: t.title,
|
|
25
20
|
heading: t.title,
|
|
26
|
-
body:
|
|
21
|
+
body: appParagraph(message) + appHint(t.hint),
|
|
27
22
|
});
|
|
28
23
|
}
|
|
29
24
|
/** CSP of the refusal page: the shell's style only, nothing runs. */
|
|
30
|
-
|
|
31
|
-
function registerAppRefusedRoute(fastify) {
|
|
32
|
-
fastify.get(
|
|
25
|
+
export const APP_REFUSED_CSP = appPageCsp();
|
|
26
|
+
export function registerAppRefusedRoute(fastify) {
|
|
27
|
+
fastify.get(APP_REFUSED_PATH, async (request, reply) => {
|
|
33
28
|
reply.header('Cache-Control', 'no-store');
|
|
34
29
|
reply.header('Referrer-Policy', 'no-referrer');
|
|
35
|
-
reply.header('Content-Security-Policy',
|
|
30
|
+
reply.header('Content-Security-Policy', APP_REFUSED_CSP);
|
|
36
31
|
reply.type('text/html; charset=utf-8');
|
|
37
|
-
const lang =
|
|
32
|
+
const lang = resolveAppPageLang({
|
|
38
33
|
lang: request.query['lang'],
|
|
39
34
|
acceptLanguage: request.headers['accept-language'],
|
|
40
35
|
});
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_AUTH_NETWORK_POLICY = exports.TRUSTED_NETWORKS_KEY = exports.TRUSTED_PROXIES_KEY = void 0;
|
|
4
|
-
exports.createAuthNetworkPolicy = createAuthNetworkPolicy;
|
|
5
1
|
/**
|
|
6
2
|
* The two network facts the auth surface reads from settings:
|
|
7
3
|
*
|
|
@@ -17,10 +13,10 @@ exports.createAuthNetworkPolicy = createAuthNetworkPolicy;
|
|
|
17
13
|
* answer yet is treated as an EMPTY list: no extra proxy is trusted, and no
|
|
18
14
|
* network skips a factor — both fail closed.
|
|
19
15
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
import { createCidrMatcher, readCidrSetting } from '@camstack/system';
|
|
17
|
+
import { resolveClientAddress } from './client-address.js';
|
|
18
|
+
export const TRUSTED_PROXIES_KEY = 'auth.trustedProxies';
|
|
19
|
+
export const TRUSTED_NETWORKS_KEY = 'auth.trustedNetworks';
|
|
24
20
|
function asRequest(req) {
|
|
25
21
|
if (req === null || typeof req !== 'object')
|
|
26
22
|
return null;
|
|
@@ -36,7 +32,7 @@ function asRequest(req) {
|
|
|
36
32
|
}
|
|
37
33
|
function safeList(read, key) {
|
|
38
34
|
try {
|
|
39
|
-
return
|
|
35
|
+
return readCidrSetting(read(key));
|
|
40
36
|
}
|
|
41
37
|
catch {
|
|
42
38
|
return [];
|
|
@@ -45,31 +41,31 @@ function safeList(read, key) {
|
|
|
45
41
|
/** One memo per list: rebuilding a BlockList per request is wasted work. */
|
|
46
42
|
function memoMatcher() {
|
|
47
43
|
let lastKey = null;
|
|
48
|
-
let last =
|
|
44
|
+
let last = createCidrMatcher([]);
|
|
49
45
|
return (entries) => {
|
|
50
46
|
const key = entries.join('\n');
|
|
51
47
|
if (key !== lastKey) {
|
|
52
48
|
lastKey = key;
|
|
53
|
-
last =
|
|
49
|
+
last = createCidrMatcher(entries);
|
|
54
50
|
}
|
|
55
51
|
return last;
|
|
56
52
|
};
|
|
57
53
|
}
|
|
58
|
-
function createAuthNetworkPolicy(read) {
|
|
54
|
+
export function createAuthNetworkPolicy(read) {
|
|
59
55
|
const networks = memoMatcher();
|
|
60
56
|
return {
|
|
61
57
|
clientAddress(req) {
|
|
62
58
|
const request = asRequest(req);
|
|
63
59
|
if (request === null)
|
|
64
60
|
return { ip: null, peer: null, viaProxy: false };
|
|
65
|
-
return
|
|
61
|
+
return resolveClientAddress(request, { trustedProxies: safeList(read, TRUSTED_PROXIES_KEY) });
|
|
66
62
|
},
|
|
67
63
|
isTrustedNetworkLogin(address) {
|
|
68
64
|
if (address.viaProxy || address.ip === null)
|
|
69
65
|
return false;
|
|
70
|
-
return networks(safeList(read,
|
|
66
|
+
return networks(safeList(read, TRUSTED_NETWORKS_KEY)).contains(address.ip);
|
|
71
67
|
},
|
|
72
68
|
};
|
|
73
69
|
}
|
|
74
70
|
/** No settings: loopback is the only trusted proxy, no network skips a factor. */
|
|
75
|
-
|
|
71
|
+
export const DEFAULT_AUTH_NETWORK_POLICY = createAuthNetworkPolicy(() => undefined);
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AUTH_GOVERNING_CAPS = void 0;
|
|
4
|
-
exports.isAuthAddonDisabled = isAuthAddonDisabled;
|
|
5
|
-
exports.bridgeProviderAddonId = bridgeProviderAddonId;
|
|
6
1
|
/** Capabilities whose per-provider toggle is an auth addon's enable. */
|
|
7
|
-
|
|
2
|
+
export const AUTH_GOVERNING_CAPS = ['auth-provider', 'user-passkeys'];
|
|
8
3
|
/** Whether the operator disabled `addonId` on Identity → Authentication. */
|
|
9
|
-
function isAuthAddonDisabled(registry, addonId) {
|
|
10
|
-
return
|
|
4
|
+
export function isAuthAddonDisabled(registry, addonId) {
|
|
5
|
+
return AUTH_GOVERNING_CAPS.some((cap) => registry.isProviderDisabled(cap, addonId));
|
|
11
6
|
}
|
|
12
7
|
/**
|
|
13
8
|
* The addon behind an SSO bridge's `provider` claim: `auth-oidc/<instance>`
|
|
14
9
|
* is signed by `auth-oidc`, anything else names its addon directly.
|
|
15
10
|
*/
|
|
16
|
-
function bridgeProviderAddonId(provider) {
|
|
11
|
+
export function bridgeProviderAddonId(provider) {
|
|
17
12
|
const slash = provider.indexOf('/');
|
|
18
13
|
return slash === -1 ? provider : provider.slice(0, slash);
|
|
19
14
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createRateLimiter = createRateLimiter;
|
|
4
|
-
exports.clientKeyFromRequest = clientKeyFromRequest;
|
|
5
1
|
/**
|
|
6
2
|
* Public-auth rate limiting — fixed window per (bucket, client ip).
|
|
7
3
|
*
|
|
@@ -16,8 +12,8 @@ exports.clientKeyFromRequest = clientKeyFromRequest;
|
|
|
16
12
|
* BOUNDED (`maxKeys`, oldest-window evicted first) so an attacker
|
|
17
13
|
* rotating spoofed source addresses cannot grow it without limit.
|
|
18
14
|
*/
|
|
19
|
-
|
|
20
|
-
function createRateLimiter(options) {
|
|
15
|
+
import { DEFAULT_AUTH_NETWORK_POLICY } from './auth-network-policy.js';
|
|
16
|
+
export function createRateLimiter(options) {
|
|
21
17
|
const maxKeys = options.maxKeys ?? 10_000;
|
|
22
18
|
const now = options.now ?? Date.now;
|
|
23
19
|
const entries = new Map();
|
|
@@ -76,6 +72,6 @@ function createRateLimiter(options) {
|
|
|
76
72
|
* bucket locked the whole internet out. `null` = no originating request
|
|
77
73
|
* (a mesh-internal `createCaller`) — never limited.
|
|
78
74
|
*/
|
|
79
|
-
function clientKeyFromRequest(req, policy =
|
|
75
|
+
export function clientKeyFromRequest(req, policy = DEFAULT_AUTH_NETWORK_POLICY) {
|
|
80
76
|
return policy.clientAddress(req).ip;
|
|
81
77
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveClientAddress = resolveClientAddress;
|
|
4
1
|
/**
|
|
5
2
|
* The real client address of an HTTP / WS request.
|
|
6
3
|
*
|
|
@@ -25,8 +22,8 @@ exports.resolveClientAddress = resolveClientAddress;
|
|
|
25
22
|
* second factor from a trusted network) refuses `viaProxy` outright, whatever
|
|
26
23
|
* address the headers name.
|
|
27
24
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
import { createCidrMatcher, normalizeIp } from '@camstack/system';
|
|
26
|
+
import { isIP } from 'node:net';
|
|
30
27
|
const LOOPBACK = ['127.0.0.0/8', '::1/128'];
|
|
31
28
|
function header(req, name) {
|
|
32
29
|
const raw = req.headers[name];
|
|
@@ -34,15 +31,15 @@ function header(req, name) {
|
|
|
34
31
|
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
35
32
|
}
|
|
36
33
|
function validIp(value) {
|
|
37
|
-
const ip =
|
|
38
|
-
return
|
|
34
|
+
const ip = normalizeIp(value);
|
|
35
|
+
return isIP(ip) === 0 ? null : ip;
|
|
39
36
|
}
|
|
40
|
-
function resolveClientAddress(req, config) {
|
|
37
|
+
export function resolveClientAddress(req, config) {
|
|
41
38
|
const rawPeer = req.socket?.remoteAddress;
|
|
42
39
|
const peer = typeof rawPeer === 'string' && rawPeer.length > 0 ? validIp(rawPeer) : null;
|
|
43
40
|
if (peer === null)
|
|
44
41
|
return { ip: null, peer: null, viaProxy: false };
|
|
45
|
-
const trusted =
|
|
42
|
+
const trusted = createCidrMatcher([...LOOPBACK, ...config.trustedProxies]);
|
|
46
43
|
if (!trusted.contains(peer))
|
|
47
44
|
return { ip: peer, peer, viaProxy: false };
|
|
48
45
|
// Cloudflare stamps the connecting client itself; prefer it when it parses.
|