@plitzi/sdk-server 0.33.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +283 -8
- package/dist/actions.d.ts +62 -0
- package/dist/actions.js +12 -0
- package/dist/adapters/cloudAdapters.d.ts +72 -0
- package/dist/adapters/cloudAdapters.js +303 -0
- package/dist/adapters/space/index.d.ts +93 -0
- package/dist/adapters/space/index.js +122 -0
- package/dist/adapters/space/resolvers.d.ts +69 -0
- package/dist/adapters/space/resolvers.js +79 -0
- package/dist/adapters/space/space.test.d.ts +1 -0
- package/dist/adapters/space/types.d.ts +64 -0
- package/dist/adapters/space/types.js +41 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +19 -0
- package/dist/core/auth/api.d.ts +400 -11
- package/dist/core/auth/api.js +1044 -50
- package/dist/core/auth/createAuth.d.ts +135 -30
- package/dist/core/auth/createAuth.js +96 -24
- package/dist/core/auth/credentials.d.ts +9 -3
- package/dist/core/auth/credentials.js +10 -4
- package/dist/core/auth/csrf.d.ts +89 -0
- package/dist/core/auth/csrf.js +234 -0
- package/dist/core/auth/csrf.test.d.ts +1 -0
- package/dist/core/auth/identity.d.ts +6 -0
- package/dist/core/auth/identity.js +37 -8
- package/dist/core/auth/lifecycle.test.d.ts +1 -0
- package/dist/core/auth/oauth/index.d.ts +8 -0
- package/dist/core/auth/oauth/index.js +13 -15
- package/dist/core/auth/passwords.d.ts +15 -0
- package/dist/core/auth/passwords.js +32 -0
- package/dist/core/auth/redirects.d.ts +18 -0
- package/dist/core/auth/redirects.js +31 -0
- package/dist/core/auth/redirects.test.d.ts +1 -0
- package/dist/core/auth/routes.d.ts +4 -2
- package/dist/core/auth/routes.js +234 -9
- package/dist/core/auth/session.d.ts +11 -0
- package/dist/core/auth/session.js +1 -1
- package/dist/core/auth/spaceTokens.d.ts +38 -2
- package/dist/core/auth/spaceTokens.js +54 -2
- package/dist/core/auth/throttle.d.ts +5 -0
- package/dist/core/auth/throttle.js +81 -0
- package/dist/core/auth/throttle.test.d.ts +1 -0
- package/dist/core/auth/tokens.d.ts +83 -12
- package/dist/core/auth/tokens.js +51 -9
- package/dist/core/auth/totp.d.ts +48 -0
- package/dist/core/auth/totp.js +118 -0
- package/dist/core/auth/totp.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +0 -6
- package/dist/core/createServer.js +35 -3
- package/dist/core/fontStore.d.ts +33 -0
- package/dist/core/fontStore.js +53 -0
- package/dist/core/fontStore.test.d.ts +1 -0
- package/dist/core/handlers/authMiddleware.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
- package/dist/core/handlers/authRouteHandlers.js +4 -3
- package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
- package/dist/core/handlers/csrfMiddleware.js +32 -0
- package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
- package/dist/core/handlers/oauthRouteHandlers.js +159 -0
- package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
- package/dist/core/handlers/socialRouteHandlers.js +129 -0
- package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
- package/dist/core/handlers/types.d.ts +22 -5
- package/dist/core/health.d.ts +11 -1
- package/dist/core/health.js +27 -3
- package/dist/core/http/dispatcher.d.ts +1 -1
- package/dist/core/http/dispatcher.js +20 -1
- package/dist/core/http/stages/authApi.js +1 -1
- package/dist/core/http/stages/fontAssets.d.ts +9 -0
- package/dist/core/http/stages/fontAssets.js +27 -0
- package/dist/core/http/stages/oauth.d.ts +19 -0
- package/dist/core/http/stages/oauth.js +118 -0
- package/dist/core/http/types.d.ts +12 -0
- package/dist/core/oauth/authorize.d.ts +22 -0
- package/dist/core/oauth/authorize.js +254 -0
- package/dist/core/oauth/authorize.test.d.ts +1 -0
- package/dist/core/oauth/challenge.d.ts +11 -0
- package/dist/core/oauth/challenge.js +33 -0
- package/dist/core/oauth/consentPage.d.ts +12 -0
- package/dist/core/oauth/consentPage.js +131 -0
- package/dist/core/oauth/metadata.d.ts +41 -0
- package/dist/core/oauth/metadata.js +71 -0
- package/dist/core/oauth/params.d.ts +5 -0
- package/dist/core/oauth/params.js +5 -0
- package/dist/core/oauth/pkce.d.ts +6 -0
- package/dist/core/oauth/pkce.js +15 -0
- package/dist/core/oauth/records.d.ts +72 -0
- package/dist/core/oauth/records.js +49 -0
- package/dist/core/oauth/register.d.ts +5 -0
- package/dist/core/oauth/register.js +55 -0
- package/dist/core/oauth/respond.d.ts +21 -0
- package/dist/core/oauth/respond.js +59 -0
- package/dist/core/oauth/token.d.ts +14 -0
- package/dist/core/oauth/token.js +130 -0
- package/dist/core/previewToken.d.ts +20 -0
- package/dist/core/previewToken.js +29 -1
- package/dist/core/server/pageServer.js +4 -1
- package/dist/core/services/action.d.ts +6 -4
- package/dist/core/services/action.js +103 -10
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/resolve.d.ts +8 -0
- package/dist/core/services/resolve.js +12 -1
- package/dist/core/staticFiles.js +1 -2
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +4 -1
- package/dist/helpers/buildResponseHelpers.d.ts +8 -0
- package/dist/helpers/buildResponseHelpers.js +9 -1
- package/dist/helpers/buildServerInfo.d.ts +8 -3
- package/dist/helpers/buildServerInfo.js +18 -14
- package/dist/helpers/cache/keys.d.ts +13 -0
- package/dist/helpers/cache/keys.js +12 -1
- package/dist/helpers/onAbort.d.ts +12 -0
- package/dist/helpers/onAbort.js +22 -0
- package/dist/helpers/onAbort.test.d.ts +1 -0
- package/dist/helpers/outboundGuard.d.ts +27 -0
- package/dist/helpers/outboundGuard.js +49 -0
- package/dist/helpers/outboundGuard.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +6 -3
- package/dist/helpers/serverLog.js +20 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -4
- package/dist/kernel.d.ts +5 -22
- package/dist/kernel.js +1 -14
- package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
- package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
- package/dist/modules/actions/index.d.ts +40 -0
- package/dist/modules/actions/index.js +46 -0
- package/dist/modules/actions/moduleFor.d.ts +3 -0
- package/dist/modules/actions/moduleFor.js +35 -0
- package/dist/modules/actions/moduleFor.test.d.ts +1 -0
- package/dist/modules/actions/runtime/check.d.ts +24 -0
- package/dist/modules/actions/runtime/check.js +111 -0
- package/dist/modules/actions/runtime/check.test.d.ts +1 -0
- package/dist/modules/actions/runtime/errors.d.ts +11 -0
- package/dist/modules/actions/runtime/errors.js +17 -0
- package/dist/modules/actions/runtime/guards.d.ts +126 -0
- package/dist/modules/actions/runtime/guards.js +246 -0
- package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
- package/dist/modules/actions/runtime/kvStore.js +41 -0
- package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
- package/dist/modules/actions/runtime/limits.d.ts +11 -0
- package/dist/modules/actions/runtime/limits.js +35 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
- package/dist/modules/actions/runtime/memoryKv.js +77 -0
- package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
- package/dist/modules/actions/runtime/namespaceKv.js +19 -0
- package/dist/modules/actions/runtime/precheck.d.ts +27 -0
- package/dist/modules/actions/runtime/precheck.js +45 -0
- package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
- package/dist/modules/actions/runtime/renderResolver.js +130 -0
- package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
- package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
- package/dist/modules/actions/runtime/renderShare.js +36 -0
- package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
- package/dist/modules/actions/runtime/report.d.ts +14 -0
- package/dist/modules/actions/runtime/report.js +22 -0
- package/dist/modules/actions/runtime/runAction.d.ts +12 -0
- package/dist/modules/actions/runtime/runAction.js +405 -0
- package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
- package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
- package/dist/modules/actions/runtime/runLogger.js +66 -0
- package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
- package/dist/modules/actions/runtime/schedule.d.ts +35 -0
- package/dist/modules/actions/runtime/schedule.js +76 -0
- package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
- package/dist/modules/actions/runtime/scope.d.ts +40 -0
- package/dist/modules/actions/runtime/scope.js +103 -0
- package/dist/modules/actions/runtime/triggers.d.ts +12 -0
- package/dist/modules/actions/runtime/triggers.js +14 -0
- package/dist/modules/actions/taskCatalog.d.ts +26 -0
- package/dist/modules/actions/taskCatalog.js +24 -0
- package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
- package/dist/modules/actions/tasks/auth.d.ts +2 -0
- package/dist/modules/actions/tasks/auth.js +40 -0
- package/dist/modules/actions/tasks/builtins.d.ts +8 -0
- package/dist/modules/actions/tasks/builtins.js +24 -0
- package/dist/modules/actions/tasks/connector.d.ts +2 -0
- package/dist/modules/actions/tasks/connector.js +141 -0
- package/dist/modules/actions/tasks/db.d.ts +2 -0
- package/dist/modules/actions/tasks/db.js +59 -0
- package/dist/modules/actions/tasks/db.test.d.ts +1 -0
- package/dist/modules/actions/tasks/flow.d.ts +3 -0
- package/dist/modules/actions/tasks/flow.js +109 -0
- package/dist/modules/actions/tasks/helpers.d.ts +16 -0
- package/dist/modules/actions/tasks/helpers.js +30 -0
- package/dist/modules/actions/tasks/http.d.ts +2 -0
- package/dist/modules/actions/tasks/http.js +111 -0
- package/dist/modules/actions/tasks/http.test.d.ts +1 -0
- package/dist/modules/actions/tasks/kv.d.ts +2 -0
- package/dist/modules/actions/tasks/kv.js +102 -0
- package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -0
- package/dist/modules/actions/tasks/registry.js +36 -0
- package/dist/modules/actions/tasks/transform.d.ts +2 -0
- package/dist/modules/actions/tasks/transform.js +34 -0
- package/dist/modules/actions/transport/callHandler.d.ts +24 -0
- package/dist/modules/actions/transport/callHandler.js +233 -0
- package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
- package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
- package/dist/modules/actions/transport/cancelHandler.js +23 -0
- package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
- package/dist/modules/actions/transport/catalogHandler.js +21 -0
- package/dist/modules/actions/transport/stream.d.ts +23 -0
- package/dist/modules/actions/transport/stream.js +100 -0
- package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
- package/dist/modules/actions/transport/verifySignature.js +72 -0
- package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
- package/dist/modules/actions/transport/webhookHandler.js +206 -0
- package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
- package/dist/modules/actions/types.d.ts +234 -0
- package/dist/modules/connectors/engine.js +4 -1
- package/dist/modules/connectors/resolver.d.ts +10 -2
- package/dist/modules/connectors/resolver.js +8 -3
- package/dist/modules/mysql/accounts.d.ts +5 -0
- package/dist/modules/mysql/accounts.js +365 -0
- package/dist/modules/mysql/accounts.test.d.ts +1 -0
- package/dist/modules/mysql/admin.d.ts +58 -0
- package/dist/modules/mysql/admin.js +133 -0
- package/dist/modules/mysql/config.d.ts +82 -0
- package/dist/modules/mysql/config.js +30 -0
- package/dist/modules/mysql/identities.d.ts +35 -0
- package/dist/modules/mysql/identities.js +43 -0
- package/dist/modules/mysql/index.d.ts +67 -0
- package/dist/modules/mysql/index.js +62 -0
- package/dist/modules/mysql/pool.d.ts +3 -0
- package/dist/modules/mysql/pool.js +88 -0
- package/dist/modules/mysql/query.d.ts +12 -0
- package/dist/modules/mysql/query.js +21 -0
- package/dist/modules/mysql/schema.d.ts +43 -0
- package/dist/modules/mysql/schema.js +340 -0
- package/dist/modules/mysql/schema.test.d.ts +1 -0
- package/dist/modules/mysql/sessions.d.ts +33 -0
- package/dist/modules/mysql/sessions.js +46 -0
- package/dist/modules/mysql/spaceTokens.d.ts +11 -0
- package/dist/modules/mysql/spaceTokens.js +59 -0
- package/dist/modules/mysql/store.integration.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +25 -0
- package/dist/modules/rsc/connectorRscData.js +51 -0
- package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
- package/dist/modules/rsc/handler.js +16 -3
- package/dist/modules/rsc/matchRscPage.d.ts +14 -0
- package/dist/modules/rsc/matchRscPage.js +26 -0
- package/dist/modules/rsc/resolveRscData.d.ts +14 -1
- package/dist/modules/rsc/resolveRscData.js +23 -34
- package/dist/modules/ssr/Component.d.ts +6 -2
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/draftSession.test.d.ts +1 -0
- package/dist/modules/ssr/handler.js +15 -2
- package/dist/modules/ssr/loadPluginComponents.js +22 -5
- package/dist/modules/ssr/prepareRender.js +87 -8
- package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
- package/dist/modules/ssr/preview.d.ts +21 -8
- package/dist/modules/ssr/preview.js +38 -14
- package/dist/modules/ssr/registerExternalPlugins.js +2 -2
- package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
- package/dist/modules/ssr/resolvePageSeo.js +26 -0
- package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
- package/dist/modules/ssr/template.test.d.ts +1 -0
- package/dist/modules/ssr/views/template.ejs +71 -5
- package/dist/mysql.d.ts +26 -0
- package/dist/mysql.js +4 -0
- package/dist/oauth.d.ts +24 -0
- package/dist/oauth.js +10 -0
- package/dist/plugins/compile.d.ts +1 -0
- package/dist/plugins/compile.js +37 -3
- package/dist/plugins/manager.d.ts +40 -0
- package/dist/plugins/manager.js +113 -13
- package/dist/plugins/manager.test.d.ts +1 -0
- package/package.json +43 -14
- package/skills/plitzi-authoring/SKILL.md +252 -0
- /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
- /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
|
@@ -18,11 +18,40 @@ export declare const SCOPES: {
|
|
|
18
18
|
readonly widget: "widget";
|
|
19
19
|
readonly spaceRender: "space:render";
|
|
20
20
|
readonly spaceAgent: "space:agent";
|
|
21
|
+
/**
|
|
22
|
+
* A SERVER that renders this space as its own — self-hosting. Secret by construction, which is the whole reason it
|
|
23
|
+
* exists apart from `space:render`: see {@link SpaceScope}.
|
|
24
|
+
*/
|
|
25
|
+
readonly spaceHost: "space:host";
|
|
26
|
+
/** A sign-in that got the password right and still owes a second factor. Carries no authority of its own. */
|
|
27
|
+
readonly mfa: "mfa";
|
|
21
28
|
};
|
|
22
29
|
export type TokenScope = (typeof SCOPES)[keyof typeof SCOPES];
|
|
23
|
-
|
|
30
|
+
/**
|
|
31
|
+
* What a space credential IS, and the distinction the other two are defined against.
|
|
32
|
+
*
|
|
33
|
+
* `render` is **public**: it ships in the clear inside every published page, so anybody who views source has it. The
|
|
34
|
+
* only thing that keeps a copied one from working is that a browser is made to state where it is presenting from and
|
|
35
|
+
* that statement is checked — a control that exists solely because browsers enforce it, and that means nothing to a
|
|
36
|
+
* client which simply does not send the header.
|
|
37
|
+
*
|
|
38
|
+
* `host` is that same read access for a server, and it is **secret**: shown once when it is issued, never embedded in
|
|
39
|
+
* a page, held only by the deployment operator. Self-hosting needs a credential that survives having no Origin to
|
|
40
|
+
* check, and a public one cannot be it — a render key lifted from someone else's page would otherwise be enough to
|
|
41
|
+
* serve a byte-identical clone of their site from anywhere.
|
|
42
|
+
*
|
|
43
|
+
* `agent` writes, on behalf of the member who consented.
|
|
44
|
+
*/
|
|
45
|
+
export type SpaceScope = 'render' | 'agent' | 'host';
|
|
46
|
+
/** The scopes that are safe to present with no `Origin` header, because they are not public to begin with. */
|
|
47
|
+
export declare const OFF_BROWSER_SPACE_SCOPES: readonly SpaceScope[];
|
|
24
48
|
/** Why a credential was refused. A reason rather than a status, because each transport maps it onto its own. */
|
|
25
|
-
export type AuthFailure = 'missing' | 'malformed' | 'expired' | 'outdated' | 'revoked' | 'wrong-type' | 'scope-mismatch' | 'issuer-not-allowed' | 'origin-not-allowed' | 'domain-not-allowed' | 'inactive'
|
|
49
|
+
export type AuthFailure = 'missing' | 'malformed' | 'expired' | 'outdated' | 'revoked' | 'wrong-type' | 'scope-mismatch' | 'issuer-not-allowed' | 'origin-not-allowed' | 'domain-not-allowed' | 'inactive'
|
|
50
|
+
/**
|
|
51
|
+
* The password was right and the address has never answered. Separate from `inactive` because it is the one
|
|
52
|
+
* refusal whose owner can clear it themselves, and a screen that cannot tell them apart has to guess.
|
|
53
|
+
*/
|
|
54
|
+
| 'unverified';
|
|
26
55
|
export declare const authFailureMessage: Record<AuthFailure, string>;
|
|
27
56
|
/**
|
|
28
57
|
* How long each credential lives. One rule, applied by how much damage the credential can do and whether something
|
|
@@ -40,8 +69,25 @@ export declare const DEFAULT_LIFETIMES: {
|
|
|
40
69
|
readonly refresh: number;
|
|
41
70
|
readonly agent: number;
|
|
42
71
|
readonly widget: number;
|
|
72
|
+
/**
|
|
73
|
+
* Long enough to fetch a code out of an app, short enough that a stolen one is worthless. It authorises nothing
|
|
74
|
+
* on its own — only finishing the sign-in it belongs to, which still needs the code.
|
|
75
|
+
*/
|
|
76
|
+
readonly mfaChallenge: 300;
|
|
77
|
+
/**
|
|
78
|
+
* The longest a session may live no matter how often it renews, counted from when it began. `0` means no cap,
|
|
79
|
+
* which is the default and what most deployments want: the renewal window is already an idle timeout, since a
|
|
80
|
+
* session nobody refreshes dies with its refresh token. This is the other half — the one that ends a session
|
|
81
|
+
* that has been renewing quietly for a year.
|
|
82
|
+
*/
|
|
83
|
+
readonly session: 0;
|
|
43
84
|
};
|
|
44
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Seconds, per credential. Written as a record of `number` rather than `Partial<typeof DEFAULT_LIFETIMES>`: the
|
|
87
|
+
* defaults are `as const`, so that shape narrowed every field to the literal it happens to default to and made
|
|
88
|
+
* `{ access: 3600 }` a type error. The option existed and could not be used.
|
|
89
|
+
*/
|
|
90
|
+
export type TokenLifetimes = Partial<Record<keyof typeof DEFAULT_LIFETIMES, number>>;
|
|
45
91
|
export type TokenConfig = {
|
|
46
92
|
secret: string;
|
|
47
93
|
/** What this deployment mints under. Also accepted on the way back in, along with `alsoAccept`. */
|
|
@@ -70,13 +116,33 @@ interface BaseClaims {
|
|
|
70
116
|
export interface UserTokenPayload extends BaseClaims {
|
|
71
117
|
scope: typeof SCOPES.user;
|
|
72
118
|
sub: string;
|
|
119
|
+
/**
|
|
120
|
+
* Who is really behind this credential, when it is not the subject: an administrator acting AS somebody. `act` is
|
|
121
|
+
* the registered claim for exactly that (RFC 8693 §4.1), so a log line, an audit trail or a downstream service can
|
|
122
|
+
* tell a session somebody was given from one they signed into — and nothing has to invent a claim to say it.
|
|
123
|
+
*/
|
|
124
|
+
act?: {
|
|
125
|
+
sub: string;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** What a session credential is, beyond who it is for. */
|
|
129
|
+
export interface UserTokenOptions {
|
|
130
|
+
/** The id of whoever obtained this session on the subject's behalf. Becomes the `act` claim. */
|
|
131
|
+
actingAs?: number | string;
|
|
132
|
+
/** Overrides `lifetimes.access`. A borrowed session is worth keeping short. */
|
|
133
|
+
ttlSeconds?: number;
|
|
73
134
|
}
|
|
74
135
|
export interface RefreshTokenPayload extends BaseClaims {
|
|
75
136
|
scope: typeof SCOPES.refresh;
|
|
76
137
|
sub: string;
|
|
77
138
|
}
|
|
139
|
+
/** Half a sign-in: the password is proven, the second factor is not. Authorises finishing that login and nothing else. */
|
|
140
|
+
export interface MfaChallengePayload extends BaseClaims {
|
|
141
|
+
scope: typeof SCOPES.mfa;
|
|
142
|
+
sub: string;
|
|
143
|
+
}
|
|
78
144
|
export interface SpaceTokenPayload extends BaseClaims {
|
|
79
|
-
scope: typeof SCOPES.spaceRender | typeof SCOPES.spaceAgent;
|
|
145
|
+
scope: typeof SCOPES.spaceRender | typeof SCOPES.spaceAgent | typeof SCOPES.spaceHost;
|
|
80
146
|
sub: string;
|
|
81
147
|
/** Web origins the space may be embedded on. No registered claim covers this. */
|
|
82
148
|
origins: string[];
|
|
@@ -85,7 +151,7 @@ export interface SpaceTokenPayload extends BaseClaims {
|
|
|
85
151
|
export interface WidgetTokenPayload extends BaseClaims {
|
|
86
152
|
scope: typeof SCOPES.widget;
|
|
87
153
|
}
|
|
88
|
-
export type TokenPayload = UserTokenPayload | RefreshTokenPayload | SpaceTokenPayload | WidgetTokenPayload;
|
|
154
|
+
export type TokenPayload = UserTokenPayload | RefreshTokenPayload | SpaceTokenPayload | WidgetTokenPayload | MfaChallengePayload;
|
|
89
155
|
export type VerifyResult<T> = {
|
|
90
156
|
ok: true;
|
|
91
157
|
payload: T;
|
|
@@ -108,15 +174,14 @@ export interface SpaceTokenOptions {
|
|
|
108
174
|
* server issues under their own and nothing they mint is worth anything here, or the other way round.
|
|
109
175
|
*/
|
|
110
176
|
export declare const createTokens: (config: TokenConfig) => {
|
|
111
|
-
lifetimes:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
widget: number;
|
|
116
|
-
};
|
|
117
|
-
generateUserToken: (userId: number | string) => string;
|
|
177
|
+
lifetimes: Record<"session" | "refresh" | "widget" | "agent" | "access" | "mfaChallenge", number>;
|
|
178
|
+
/** What this deployment mints under. Read by anything that has to name the deployment — an enrolment URI, say. */
|
|
179
|
+
issuer: string;
|
|
180
|
+
generateUserToken: (userId: number | string, { actingAs, ttlSeconds }?: UserTokenOptions) => string;
|
|
118
181
|
generateRefreshToken: (userId: number | string) => string;
|
|
119
182
|
generateWidgetToken: () => string;
|
|
183
|
+
/** Says "this person proved their password"; nothing more, and only for a few minutes. */
|
|
184
|
+
generateMfaChallenge: (userId: number | string) => string;
|
|
120
185
|
/**
|
|
121
186
|
* A `render` token may live forever, and defaults to it, because it is embedded in a published site — often a
|
|
122
187
|
* SPA deployed once and left alone — where an expiry is a scheduled outage: the site breaks weeks later with
|
|
@@ -127,10 +192,16 @@ export declare const createTokens: (config: TokenConfig) => {
|
|
|
127
192
|
*
|
|
128
193
|
* An `agent` grant is the opposite default and always carries a lifetime: it writes, and it is held by a
|
|
129
194
|
* third-party host.
|
|
195
|
+
*
|
|
196
|
+
* A `host` grant follows `render`: it only reads, it is held by the space owner's own deployment, and an expiry
|
|
197
|
+
* there is the same scheduled outage — except the site goes quietly STALE rather than dark, because a
|
|
198
|
+
* self-hosted renderer keeps serving the last copy it fetched. Being secret, it is worth rotating; being
|
|
199
|
+
* revocable, it does not need a deadline to be withdrawn.
|
|
130
200
|
*/
|
|
131
201
|
generateSpaceToken: (spaceId: number | string, origins: string[], scope?: SpaceScope, { expiresAt }?: SpaceTokenOptions) => string;
|
|
132
202
|
verifyUserToken: (token: string) => VerifyResult<UserTokenPayload>;
|
|
133
203
|
verifyRefreshToken: (token: string) => VerifyResult<RefreshTokenPayload>;
|
|
204
|
+
verifyMfaChallenge: (token: string) => VerifyResult<MfaChallengePayload>;
|
|
134
205
|
verifyWidgetToken: (token: string) => VerifyResult<WidgetTokenPayload>;
|
|
135
206
|
verifySpaceToken: (token: string) => VerifyResult<SpaceTokenPayload & {
|
|
136
207
|
spaceId: number;
|
package/dist/core/auth/tokens.js
CHANGED
|
@@ -19,8 +19,17 @@ var SCOPES = {
|
|
|
19
19
|
refresh: "refresh",
|
|
20
20
|
widget: "widget",
|
|
21
21
|
spaceRender: "space:render",
|
|
22
|
-
spaceAgent: "space:agent"
|
|
22
|
+
spaceAgent: "space:agent",
|
|
23
|
+
/**
|
|
24
|
+
* A SERVER that renders this space as its own — self-hosting. Secret by construction, which is the whole reason it
|
|
25
|
+
* exists apart from `space:render`: see {@link SpaceScope}.
|
|
26
|
+
*/
|
|
27
|
+
spaceHost: "space:host",
|
|
28
|
+
/** A sign-in that got the password right and still owes a second factor. Carries no authority of its own. */
|
|
29
|
+
mfa: "mfa"
|
|
23
30
|
};
|
|
31
|
+
/** The scopes that are safe to present with no `Origin` header, because they are not public to begin with. */
|
|
32
|
+
var OFF_BROWSER_SPACE_SCOPES = ["agent", "host"];
|
|
24
33
|
var authFailureMessage = {
|
|
25
34
|
missing: "Authentication required",
|
|
26
35
|
malformed: "Token Invalid",
|
|
@@ -32,7 +41,8 @@ var authFailureMessage = {
|
|
|
32
41
|
"issuer-not-allowed": "Token was issued by another environment",
|
|
33
42
|
"origin-not-allowed": "Access Not Authorized",
|
|
34
43
|
"domain-not-allowed": "This token is not allowed on this domain",
|
|
35
|
-
inactive: "Account is not active"
|
|
44
|
+
inactive: "Account is not active",
|
|
45
|
+
unverified: "Account is not verified"
|
|
36
46
|
};
|
|
37
47
|
var DAY = 86400;
|
|
38
48
|
/**
|
|
@@ -50,15 +60,29 @@ var DEFAULT_LIFETIMES = {
|
|
|
50
60
|
access: DAY,
|
|
51
61
|
refresh: 30 * DAY,
|
|
52
62
|
agent: 30 * DAY,
|
|
53
|
-
widget: 30 * DAY
|
|
63
|
+
widget: 30 * DAY,
|
|
64
|
+
/**
|
|
65
|
+
* Long enough to fetch a code out of an app, short enough that a stolen one is worthless. It authorises nothing
|
|
66
|
+
* on its own — only finishing the sign-in it belongs to, which still needs the code.
|
|
67
|
+
*/
|
|
68
|
+
mfaChallenge: 300,
|
|
69
|
+
/**
|
|
70
|
+
* The longest a session may live no matter how often it renews, counted from when it began. `0` means no cap,
|
|
71
|
+
* which is the default and what most deployments want: the renewal window is already an idle timeout, since a
|
|
72
|
+
* session nobody refreshes dies with its refresh token. This is the other half — the one that ends a session
|
|
73
|
+
* that has been renewing quietly for a year.
|
|
74
|
+
*/
|
|
75
|
+
session: 0
|
|
54
76
|
};
|
|
55
77
|
var spaceScopeToClaim = {
|
|
56
78
|
render: SCOPES.spaceRender,
|
|
57
|
-
agent: SCOPES.spaceAgent
|
|
79
|
+
agent: SCOPES.spaceAgent,
|
|
80
|
+
host: SCOPES.spaceHost
|
|
58
81
|
};
|
|
59
82
|
var claimToSpaceScope = {
|
|
60
83
|
[SCOPES.spaceRender]: "render",
|
|
61
|
-
[SCOPES.spaceAgent]: "agent"
|
|
84
|
+
[SCOPES.spaceAgent]: "agent",
|
|
85
|
+
[SCOPES.spaceHost]: "host"
|
|
62
86
|
};
|
|
63
87
|
var isLegacyShape = (payload) => "data" in payload || "version" in payload;
|
|
64
88
|
var subjectId = (payload) => {
|
|
@@ -146,15 +170,23 @@ var createTokens = (config) => {
|
|
|
146
170
|
};
|
|
147
171
|
return {
|
|
148
172
|
lifetimes,
|
|
149
|
-
|
|
173
|
+
/** What this deployment mints under. Read by anything that has to name the deployment — an enrolment URI, say. */
|
|
174
|
+
issuer: config.issuer,
|
|
175
|
+
generateUserToken: (userId, { actingAs, ttlSeconds } = {}) => sign({
|
|
150
176
|
sub: String(userId),
|
|
151
|
-
...
|
|
177
|
+
...actingAs === void 0 ? {} : { act: { sub: String(actingAs) } },
|
|
178
|
+
...baseClaims(SCOPES.user, ttlSeconds ?? lifetimes.access)
|
|
152
179
|
}),
|
|
153
180
|
generateRefreshToken: (userId) => sign({
|
|
154
181
|
sub: String(userId),
|
|
155
182
|
...baseClaims(SCOPES.refresh, lifetimes.refresh)
|
|
156
183
|
}),
|
|
157
184
|
generateWidgetToken: () => sign(baseClaims(SCOPES.widget, lifetimes.widget)),
|
|
185
|
+
/** Says "this person proved their password"; nothing more, and only for a few minutes. */
|
|
186
|
+
generateMfaChallenge: (userId) => sign({
|
|
187
|
+
sub: String(userId),
|
|
188
|
+
...baseClaims(SCOPES.mfa, lifetimes.mfaChallenge)
|
|
189
|
+
}),
|
|
158
190
|
/**
|
|
159
191
|
* A `render` token may live forever, and defaults to it, because it is embedded in a published site — often a
|
|
160
192
|
* SPA deployed once and left alone — where an expiry is a scheduled outage: the site breaks weeks later with
|
|
@@ -165,6 +197,11 @@ var createTokens = (config) => {
|
|
|
165
197
|
*
|
|
166
198
|
* An `agent` grant is the opposite default and always carries a lifetime: it writes, and it is held by a
|
|
167
199
|
* third-party host.
|
|
200
|
+
*
|
|
201
|
+
* A `host` grant follows `render`: it only reads, it is held by the space owner's own deployment, and an expiry
|
|
202
|
+
* there is the same scheduled outage — except the site goes quietly STALE rather than dark, because a
|
|
203
|
+
* self-hosted renderer keeps serving the last copy it fetched. Being secret, it is worth rotating; being
|
|
204
|
+
* revocable, it does not need a deadline to be withdrawn.
|
|
168
205
|
*/
|
|
169
206
|
generateSpaceToken: (spaceId, origins, scope = "render", { expiresAt } = {}) => {
|
|
170
207
|
const defaultTtl = scope === "agent" ? lifetimes.agent : void 0;
|
|
@@ -177,9 +214,14 @@ var createTokens = (config) => {
|
|
|
177
214
|
},
|
|
178
215
|
verifyUserToken: (token) => withSubject(verifyScoped(token, [SCOPES.user])),
|
|
179
216
|
verifyRefreshToken: (token) => withSubject(verifyScoped(token, [SCOPES.refresh])),
|
|
217
|
+
verifyMfaChallenge: (token) => withSubject(verifyScoped(token, [SCOPES.mfa])),
|
|
180
218
|
verifyWidgetToken: (token) => verifyScoped(token, [SCOPES.widget]),
|
|
181
219
|
verifySpaceToken: (token) => {
|
|
182
|
-
const result = withSubject(verifyScoped(token, [
|
|
220
|
+
const result = withSubject(verifyScoped(token, [
|
|
221
|
+
SCOPES.spaceRender,
|
|
222
|
+
SCOPES.spaceAgent,
|
|
223
|
+
SCOPES.spaceHost
|
|
224
|
+
]));
|
|
183
225
|
if (!result.ok) return result;
|
|
184
226
|
const spaceId = subjectId(result.payload);
|
|
185
227
|
const spaceScope = claimToSpaceScope[result.payload.scope];
|
|
@@ -218,4 +260,4 @@ var createTokens = (config) => {
|
|
|
218
260
|
/** The user id a session or refresh token is about. */
|
|
219
261
|
var userIdOf = (payload) => Number(payload.sub);
|
|
220
262
|
//#endregion
|
|
221
|
-
export { DEFAULT_LIFETIMES, SCOPES, authFailureMessage, createTokens, userIdOf };
|
|
263
|
+
export { DEFAULT_LIFETIMES, OFF_BROWSER_SPACE_SCOPES, SCOPES, authFailureMessage, createTokens, userIdOf };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** A 20-byte secret, which is what RFC 4226 recommends for HMAC-SHA1 and what apps are sized for. */
|
|
2
|
+
export declare const generateTotpSecret: () => string;
|
|
3
|
+
/** The code an app would be showing right now. Exported so a test does not have to reimplement the algorithm. */
|
|
4
|
+
export declare const totpCode: (secret: string, at?: number) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Is this the code?
|
|
7
|
+
*
|
|
8
|
+
* `window` steps either side are accepted, one by default — thirty seconds of tolerance for a clock that drifts
|
|
9
|
+
* and for a person who starts typing at second twenty-nine. Wider is friendlier and weaker; this is the usual
|
|
10
|
+
* trade and the same one Google Authenticator's own guidance makes.
|
|
11
|
+
*
|
|
12
|
+
* Compared in constant time: a comparison that returns on the first differing digit leaks how much of a guess was
|
|
13
|
+
* right, which turns a million-guess space into ten guesses per position.
|
|
14
|
+
*/
|
|
15
|
+
export declare const verifyTotp: (secret: string, code: string, options?: {
|
|
16
|
+
window?: number;
|
|
17
|
+
at?: number;
|
|
18
|
+
}) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The URI an authenticator app scans. `issuer` appears twice by convention — in the label and as a parameter —
|
|
21
|
+
* because different apps read different ones, and an entry that says only an email address is unidentifiable in a
|
|
22
|
+
* list of thirty.
|
|
23
|
+
*/
|
|
24
|
+
export declare const totpUri: ({ secret, account, issuer }: {
|
|
25
|
+
secret: string;
|
|
26
|
+
account: string;
|
|
27
|
+
issuer: string;
|
|
28
|
+
}) => string;
|
|
29
|
+
/**
|
|
30
|
+
* An unbiased random string over an alphabet.
|
|
31
|
+
*
|
|
32
|
+
* `randomInt` rejection-samples, which is the whole point: the obvious `byte % alphabet.length` over-represents the
|
|
33
|
+
* first `256 % length` characters, and a token that gates an account has to draw every character with equal
|
|
34
|
+
* probability. Hand-rolling that correctly is a known trap, so it is written once here.
|
|
35
|
+
*
|
|
36
|
+
* Default alphabet is Crockford-ish base32 — no `0`/`O`/`1`/`I`, so a code read aloud or off paper survives.
|
|
37
|
+
*/
|
|
38
|
+
export declare const randomCode: (length: number, alphabet?: string) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Recovery codes, for the phone that went in a river.
|
|
41
|
+
*
|
|
42
|
+
* Ten of them, each 40 bits of entropy in a shape somebody can read off paper. Generated once and shown once: they
|
|
43
|
+
* are stored hashed, so a deployment that loses them cannot print them again, which is the property that makes
|
|
44
|
+
* them worth having.
|
|
45
|
+
*/
|
|
46
|
+
export declare const generateRecoveryCodes: (count?: number) => string[];
|
|
47
|
+
/** Normalised before hashing so that case and the hyphen a person types or omits do not decide whether it works. */
|
|
48
|
+
export declare const normalizeRecoveryCode: (code: string) => string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
2
|
+
//#region src/core/auth/totp.ts
|
|
3
|
+
/**
|
|
4
|
+
* Time-based one-time passwords, RFC 6238, on the standard library.
|
|
5
|
+
*
|
|
6
|
+
* Six digits over HMAC-SHA1 in thirty-second steps, because that is what every authenticator app assumes and the
|
|
7
|
+
* point of this is that Google Authenticator, 1Password and Aegis all work without being told anything. The RFC
|
|
8
|
+
* allows SHA-256 and other digit counts; almost nothing in the wild reads them off the enrolment URI.
|
|
9
|
+
*/
|
|
10
|
+
var DIGITS = 6;
|
|
11
|
+
var STEP_SECONDS = 30;
|
|
12
|
+
/** RFC 4648 base32, which is the alphabet authenticator apps expect a secret in. No padding: apps dislike it. */
|
|
13
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
14
|
+
var base32Encode = (bytes) => {
|
|
15
|
+
let bits = 0;
|
|
16
|
+
let value = 0;
|
|
17
|
+
let output = "";
|
|
18
|
+
for (const byte of bytes) {
|
|
19
|
+
value = value << 8 | byte;
|
|
20
|
+
bits += 8;
|
|
21
|
+
while (bits >= 5) {
|
|
22
|
+
output += ALPHABET[value >>> bits - 5 & 31];
|
|
23
|
+
bits -= 5;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (bits > 0) output += ALPHABET[value << 5 - bits & 31];
|
|
27
|
+
return output;
|
|
28
|
+
};
|
|
29
|
+
var base32Decode = (secret) => {
|
|
30
|
+
const clean = secret.toUpperCase().replace(/[^A-Z2-7]/g, "");
|
|
31
|
+
const bytes = [];
|
|
32
|
+
let bits = 0;
|
|
33
|
+
let value = 0;
|
|
34
|
+
for (const character of clean) {
|
|
35
|
+
value = value << 5 | ALPHABET.indexOf(character);
|
|
36
|
+
bits += 5;
|
|
37
|
+
if (bits >= 8) {
|
|
38
|
+
bytes.push(value >>> bits - 8 & 255);
|
|
39
|
+
bits -= 8;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Buffer.from(bytes);
|
|
43
|
+
};
|
|
44
|
+
/** A 20-byte secret, which is what RFC 4226 recommends for HMAC-SHA1 and what apps are sized for. */
|
|
45
|
+
var generateTotpSecret = () => base32Encode(randomBytes(20));
|
|
46
|
+
var codeAt = (secret, counter) => {
|
|
47
|
+
const buffer = Buffer.alloc(8);
|
|
48
|
+
buffer.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
|
|
49
|
+
buffer.writeUInt32BE(counter >>> 0, 4);
|
|
50
|
+
const digest = createHmac("sha1", base32Decode(secret)).update(buffer).digest();
|
|
51
|
+
const offset = digest[digest.length - 1] & 15;
|
|
52
|
+
const binary = (digest[offset] & 127) << 24 | (digest[offset + 1] & 255) << 16 | (digest[offset + 2] & 255) << 8 | digest[offset + 3] & 255;
|
|
53
|
+
return String(binary % 10 ** DIGITS).padStart(DIGITS, "0");
|
|
54
|
+
};
|
|
55
|
+
/** The code an app would be showing right now. Exported so a test does not have to reimplement the algorithm. */
|
|
56
|
+
var totpCode = (secret, at = Date.now()) => codeAt(secret, Math.floor(at / 1e3 / STEP_SECONDS));
|
|
57
|
+
/**
|
|
58
|
+
* Is this the code?
|
|
59
|
+
*
|
|
60
|
+
* `window` steps either side are accepted, one by default — thirty seconds of tolerance for a clock that drifts
|
|
61
|
+
* and for a person who starts typing at second twenty-nine. Wider is friendlier and weaker; this is the usual
|
|
62
|
+
* trade and the same one Google Authenticator's own guidance makes.
|
|
63
|
+
*
|
|
64
|
+
* Compared in constant time: a comparison that returns on the first differing digit leaks how much of a guess was
|
|
65
|
+
* right, which turns a million-guess space into ten guesses per position.
|
|
66
|
+
*/
|
|
67
|
+
var verifyTotp = (secret, code, options = {}) => {
|
|
68
|
+
const { window = 1, at = Date.now() } = options;
|
|
69
|
+
const candidate = code.trim();
|
|
70
|
+
if (!/^\d{6}$/.test(candidate)) return false;
|
|
71
|
+
const counter = Math.floor(at / 1e3 / STEP_SECONDS);
|
|
72
|
+
const supplied = Buffer.from(candidate);
|
|
73
|
+
let matched = false;
|
|
74
|
+
for (let drift = -window; drift <= window; drift++) {
|
|
75
|
+
const expected = Buffer.from(codeAt(secret, counter + drift));
|
|
76
|
+
matched = timingSafeEqual(expected, supplied) || matched;
|
|
77
|
+
}
|
|
78
|
+
return matched;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The URI an authenticator app scans. `issuer` appears twice by convention — in the label and as a parameter —
|
|
82
|
+
* because different apps read different ones, and an entry that says only an email address is unidentifiable in a
|
|
83
|
+
* list of thirty.
|
|
84
|
+
*/
|
|
85
|
+
var totpUri = ({ secret, account, issuer }) => {
|
|
86
|
+
return `otpauth://totp/${encodeURIComponent(`${issuer}:${account}`)}?${new URLSearchParams({
|
|
87
|
+
secret,
|
|
88
|
+
issuer,
|
|
89
|
+
algorithm: "SHA1",
|
|
90
|
+
digits: String(DIGITS),
|
|
91
|
+
period: "30"
|
|
92
|
+
}).toString()}`;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* An unbiased random string over an alphabet.
|
|
96
|
+
*
|
|
97
|
+
* `randomInt` rejection-samples, which is the whole point: the obvious `byte % alphabet.length` over-represents the
|
|
98
|
+
* first `256 % length` characters, and a token that gates an account has to draw every character with equal
|
|
99
|
+
* probability. Hand-rolling that correctly is a known trap, so it is written once here.
|
|
100
|
+
*
|
|
101
|
+
* Default alphabet is Crockford-ish base32 — no `0`/`O`/`1`/`I`, so a code read aloud or off paper survives.
|
|
102
|
+
*/
|
|
103
|
+
var randomCode = (length, alphabet = ALPHABET) => Array.from({ length }, () => alphabet[randomInt(alphabet.length)]).join("");
|
|
104
|
+
/**
|
|
105
|
+
* Recovery codes, for the phone that went in a river.
|
|
106
|
+
*
|
|
107
|
+
* Ten of them, each 40 bits of entropy in a shape somebody can read off paper. Generated once and shown once: they
|
|
108
|
+
* are stored hashed, so a deployment that loses them cannot print them again, which is the property that makes
|
|
109
|
+
* them worth having.
|
|
110
|
+
*/
|
|
111
|
+
var generateRecoveryCodes = (count = 10) => Array.from({ length: count }, () => {
|
|
112
|
+
const digits = randomCode(10);
|
|
113
|
+
return `${digits.slice(0, 5)}-${digits.slice(5)}`;
|
|
114
|
+
});
|
|
115
|
+
/** Normalised before hashing so that case and the hyphen a person types or omits do not decide whether it works. */
|
|
116
|
+
var normalizeRecoveryCode = (code) => code.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
117
|
+
//#endregion
|
|
118
|
+
export { generateRecoveryCodes, generateTotpSecret, normalizeRecoveryCode, randomCode, totpCode, totpUri, verifyTotp };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,10 +20,4 @@ export type ServerConfig = Omit<SSRServerConfig, 'adapters'> & {
|
|
|
20
20
|
*/
|
|
21
21
|
auth?: Auth;
|
|
22
22
|
};
|
|
23
|
-
/** The server this package makes: pages and RSC, mounting whatever the config enables, plus any stages a
|
|
24
|
-
* companion package contributes through `extensions`. The MCP endpoint, the widget proxy and draft-preview
|
|
25
|
-
* arrive that way from `@plitzi/sdk-mcp`, which a page-only deployment never installs.
|
|
26
|
-
*
|
|
27
|
-
* A dedicated MCP server is `createServer` from `@plitzi/sdk-mcp` — it builds none of the render template,
|
|
28
|
-
* caches or plugin manager this one does. */
|
|
29
23
|
export declare const createServer: ({ auth, ...config }: ServerConfig, extensions?: PipelineExtensions) => SSRServer;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { actionsModuleFor } from "../modules/actions/moduleFor.js";
|
|
2
|
+
import { connectorRscData } from "../modules/rsc/connectorRscData.js";
|
|
1
3
|
import { createAuthApiStage } from "./http/stages/authApi.js";
|
|
2
4
|
import { resolveServices } from "./services/resolve.js";
|
|
3
5
|
import { createPageServer } from "./server/pageServer.js";
|
|
@@ -8,11 +10,41 @@ import { createPageServer } from "./server/pageServer.js";
|
|
|
8
10
|
*
|
|
9
11
|
* A dedicated MCP server is `createServer` from `@plitzi/sdk-mcp` — it builds none of the render template,
|
|
10
12
|
* caches or plugin manager this one does. */
|
|
13
|
+
/**
|
|
14
|
+
* Fills in `getRscData` from whatever can produce a server element's data — `connectors`, `action.lookups`, or
|
|
15
|
+
* both — when the deployment did not write one.
|
|
16
|
+
*
|
|
17
|
+
* The lookups are already here and the assembly is entirely this package's — a resolver over `resolveRscData`.
|
|
18
|
+
* Leaving it out meant every deployment passed the same lookups twice: once as config, for the write endpoint,
|
|
19
|
+
* and once folded by hand into an adapter, for the read.
|
|
20
|
+
*
|
|
21
|
+
* Actions count on their own: a space whose server elements name actions rather than connectors has nothing to
|
|
22
|
+
* configure under `connectors`, and keying this on that alone left its `render` elements resolving to nothing
|
|
23
|
+
* with no configuration missing anywhere.
|
|
24
|
+
*/
|
|
25
|
+
var withConnectorRsc = (config) => {
|
|
26
|
+
if (config.adapters.getRscData || !config.connectors && !config.action?.lookups) return config;
|
|
27
|
+
const module = actionsModuleFor(config);
|
|
28
|
+
const actions = module && config.action?.lookups ? {
|
|
29
|
+
lookups: config.action.lookups,
|
|
30
|
+
module
|
|
31
|
+
} : void 0;
|
|
32
|
+
return {
|
|
33
|
+
...config,
|
|
34
|
+
adapters: {
|
|
35
|
+
...config.adapters,
|
|
36
|
+
getRscData: connectorRscData(config.connectors, actions, config.rsc?.elementTimeoutMs)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
11
40
|
var createServer = ({ auth, ...config }, extensions) => {
|
|
12
|
-
if (!auth)
|
|
41
|
+
if (!auth) {
|
|
42
|
+
const resolvedConfig = withConnectorRsc(config);
|
|
43
|
+
return createPageServer(resolvedConfig, resolveServices(resolvedConfig), extensions);
|
|
44
|
+
}
|
|
13
45
|
const entries = Object.entries(config.adapters);
|
|
14
46
|
const supplied = Object.fromEntries(entries.filter(([, value]) => value !== void 0));
|
|
15
|
-
const resolved = {
|
|
47
|
+
const resolved = withConnectorRsc({
|
|
16
48
|
loginPath: false,
|
|
17
49
|
logoutPath: false,
|
|
18
50
|
exchangePath: false,
|
|
@@ -22,7 +54,7 @@ var createServer = ({ auth, ...config }, extensions) => {
|
|
|
22
54
|
...auth.ssrAdapters,
|
|
23
55
|
...supplied
|
|
24
56
|
}
|
|
25
|
-
};
|
|
57
|
+
});
|
|
26
58
|
const withAuthRoutes = {
|
|
27
59
|
...extensions,
|
|
28
60
|
preAuth: [...extensions?.preAuth ?? [], createAuthApiStage(auth, auth.basePath)]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SpaceFont } from '@plitzi/sdk-shared';
|
|
2
|
+
/** One file in the store, as the caller needs to address it afterwards. */
|
|
3
|
+
export type StoredFont = {
|
|
4
|
+
/** Store-relative, and exactly what goes into a `hosted` font's `files[].path`. */
|
|
5
|
+
path: string;
|
|
6
|
+
size: number;
|
|
7
|
+
format: 'woff2' | 'woff';
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Where a deployment keeps the font files a space uploaded.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately not a URL builder: addressing is `fontUrlResolver(baseUrl)`, which every render surface already
|
|
13
|
+
* shares, and a store that also decided URLs would put this deployment's origin inside a document that travels to
|
|
14
|
+
* other deployments (see `HostedFont`). Nor does it read bytes back — a local store's files are served off disk by
|
|
15
|
+
* `fontAssetsStage` and a cloud store's by its CDN, so nothing would ever call it.
|
|
16
|
+
*/
|
|
17
|
+
export type FontStore = {
|
|
18
|
+
list: (spaceId: number) => Promise<StoredFont[]>;
|
|
19
|
+
put: (spaceId: number, file: {
|
|
20
|
+
name: string;
|
|
21
|
+
body: Buffer;
|
|
22
|
+
format: 'woff2' | 'woff';
|
|
23
|
+
}) => Promise<StoredFont>;
|
|
24
|
+
remove: (storePath: string) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The store a self-hosted server gets for nothing: a directory on its own disk, served by this server at
|
|
28
|
+
* `/fonts/*`. No object storage, no credentials, no account — and it works with no network at all, which is the
|
|
29
|
+
* state a self-hosted deployment is allowed to be in.
|
|
30
|
+
*/
|
|
31
|
+
export declare const createLocalFontStore: (rootDir: string) => FontStore;
|
|
32
|
+
/** Every store path a manifest names, so a deployment can tell what is still referenced from what is orphaned. */
|
|
33
|
+
export declare const hostedPathsOf: (fonts: SpaceFont[]) => string[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/core/fontStore.ts
|
|
4
|
+
/** `<spaceId>/<name>` — the space segment is what keeps one space's files out of another's. */
|
|
5
|
+
var storePathOf = (spaceId, name) => `${spaceId}/${name}`;
|
|
6
|
+
/**
|
|
7
|
+
* A path that came from a document or a request, resolved inside the store and nowhere else.
|
|
8
|
+
*
|
|
9
|
+
* The manifest is data a user can write, so `..` and absolute paths reach here; without this check a font path
|
|
10
|
+
* would be a way to read any file the process can.
|
|
11
|
+
*/
|
|
12
|
+
var resolveInside = (rootDir, storePath) => {
|
|
13
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
14
|
+
const filePath = path.resolve(resolvedRoot, storePath);
|
|
15
|
+
return filePath.startsWith(resolvedRoot + path.sep) ? filePath : void 0;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The store a self-hosted server gets for nothing: a directory on its own disk, served by this server at
|
|
19
|
+
* `/fonts/*`. No object storage, no credentials, no account — and it works with no network at all, which is the
|
|
20
|
+
* state a self-hosted deployment is allowed to be in.
|
|
21
|
+
*/
|
|
22
|
+
var createLocalFontStore = (rootDir) => ({
|
|
23
|
+
list: (spaceId) => {
|
|
24
|
+
const dir = path.resolve(rootDir, String(spaceId));
|
|
25
|
+
if (!fs.existsSync(dir)) return Promise.resolve([]);
|
|
26
|
+
return Promise.resolve(fs.readdirSync(dir).filter((name) => name.endsWith(".woff2") || name.endsWith(".woff")).map((name) => ({
|
|
27
|
+
path: storePathOf(spaceId, name),
|
|
28
|
+
size: fs.statSync(path.join(dir, name)).size,
|
|
29
|
+
format: name.endsWith(".woff2") ? "woff2" : "woff"
|
|
30
|
+
})));
|
|
31
|
+
},
|
|
32
|
+
put: (spaceId, file) => {
|
|
33
|
+
const dir = path.resolve(rootDir, String(spaceId));
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
const filePath = resolveInside(rootDir, storePathOf(spaceId, file.name));
|
|
36
|
+
if (!filePath) return Promise.reject(/* @__PURE__ */ new Error("Font name escapes the store"));
|
|
37
|
+
fs.writeFileSync(filePath, file.body);
|
|
38
|
+
return Promise.resolve({
|
|
39
|
+
path: storePathOf(spaceId, file.name),
|
|
40
|
+
size: file.body.byteLength,
|
|
41
|
+
format: file.format
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
remove: (storePath) => {
|
|
45
|
+
const filePath = resolveInside(rootDir, storePath);
|
|
46
|
+
if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
/** Every store path a manifest names, so a deployment can tell what is still referenced from what is orphaned. */
|
|
51
|
+
var hostedPathsOf = (fonts) => fonts.flatMap((font) => font.source === "hosted" ? font.files.map((file) => file.path) : []);
|
|
52
|
+
//#endregion
|
|
53
|
+
export { createLocalFontStore, hostedPathsOf };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { authFailureMessage } from "../auth/tokens.js";
|
|
2
1
|
import { createAuthorizer } from "../auth/authorize.js";
|
|
2
|
+
import { authFailureMessage } from "../auth/tokens.js";
|
|
3
3
|
//#region src/core/handlers/authMiddleware.ts
|
|
4
4
|
/**
|
|
5
5
|
* The authorizer, as middleware: `(req, res, next)`.
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { HttpRoute, RouterLike } from './types';
|
|
2
2
|
import { AuthApi } from '../auth/api';
|
|
3
|
+
import { Csrf } from '../auth/csrf';
|
|
3
4
|
import { SessionCookies } from '../auth/session';
|
|
4
5
|
export interface AuthRouteHandlersOptions {
|
|
5
6
|
api: AuthApi;
|
|
6
7
|
cookies: SessionCookies;
|
|
8
|
+
/**
|
|
9
|
+
* Enforces the CSRF check on these flows, and re-issues the token when a session is granted. Pass `auth.csrf`.
|
|
10
|
+
*
|
|
11
|
+
* Omitting it leaves the `/auth` surface unprotected, which is why `createAuth` hands one over by default — a
|
|
12
|
+
* deployment that means to go without says so with `csrf: false` there rather than by forgetting here.
|
|
13
|
+
*/
|
|
14
|
+
csrf?: Csrf;
|
|
7
15
|
/** Reports a flow that threw. Without it the failure goes to `console.error`; the caller still gets a 500. */
|
|
8
16
|
onError?: (error: unknown, context: {
|
|
9
17
|
method: string;
|
|
@@ -33,7 +41,7 @@ export interface AuthRouteHandlersOptions {
|
|
|
33
41
|
* Which flows actually answer is decided by the adapters, not here — no `createAccount`, no signup, and the route
|
|
34
42
|
* reports 404 rather than failing at runtime. Mounting all of them is correct for a deployment that offers three.
|
|
35
43
|
*/
|
|
36
|
-
export declare const createAuthRouteHandlers: ({ api, cookies, onError }: AuthRouteHandlersOptions) => HttpRoute[];
|
|
44
|
+
export declare const createAuthRouteHandlers: ({ api, cookies, csrf, onError }: AuthRouteHandlersOptions) => HttpRoute[];
|
|
37
45
|
/**
|
|
38
46
|
* {@link createAuthRouteHandlers}, hung on anything with `get` and `post`.
|
|
39
47
|
*
|
|
@@ -37,16 +37,17 @@ var carrier = (req) => ({
|
|
|
37
37
|
* Which flows actually answer is decided by the adapters, not here — no `createAccount`, no signup, and the route
|
|
38
38
|
* reports 404 rather than failing at runtime. Mounting all of them is correct for a deployment that offers three.
|
|
39
39
|
*/
|
|
40
|
-
var createAuthRouteHandlers = ({ api, cookies, onError }) => authRoutes({
|
|
40
|
+
var createAuthRouteHandlers = ({ api, cookies, csrf, onError }) => authRoutes({
|
|
41
41
|
api,
|
|
42
|
-
cookies
|
|
42
|
+
cookies,
|
|
43
|
+
csrf
|
|
43
44
|
}).map(({ method, path, handler }) => ({
|
|
44
45
|
method,
|
|
45
46
|
path,
|
|
46
47
|
handle: async (req, res) => {
|
|
47
48
|
try {
|
|
48
49
|
const outcome = await handler(carrier(req));
|
|
49
|
-
applySessionOutcome(req, res, outcome, cookies);
|
|
50
|
+
applySessionOutcome(req, res, outcome, cookies, csrf);
|
|
50
51
|
res.status(outcome.ok ? outcome.status ?? 200 : outcome.status).json(outcome.body);
|
|
51
52
|
} catch (error) {
|
|
52
53
|
if (onError) onError(error, {
|