@plitzi/sdk-server 0.33.2 → 0.34.1
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 +54 -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 +259 -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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
//#region src/adapters/cloudAdapters.ts
|
|
2
|
+
/**
|
|
3
|
+
* A space fetched from Plitzi, for a server that is not Plitzi's.
|
|
4
|
+
*
|
|
5
|
+
* This is the missing half of self-hosting: `createJsonAdapters` serves a space somebody exported by hand, which
|
|
6
|
+
* goes stale the moment it is edited, and everything else meant standing up a database. Here the space stays in
|
|
7
|
+
* the builder — published, versioned, edited by whoever normally edits it — and this deployment reads it over the
|
|
8
|
+
* same GraphQL query the browser-rendered SDK uses, with the same space key. What is served is the deployment's
|
|
9
|
+
* own: its own domain, its own auth, its own actions, its own logs.
|
|
10
|
+
*
|
|
11
|
+
* **Plitzi is never in the critical path of a page.** A version is fetched once and then served from cache; the
|
|
12
|
+
* live document refreshes behind the request rather than in front of it, and a refresh that fails leaves the last
|
|
13
|
+
* good copy serving. A self-hosted site does not go down because somebody else's API did.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Plitzi's own server role, which serves GraphQL at its root — no `/graphql` path, exactly as the browser SDK
|
|
17
|
+
* addresses it. Production is the default because production is where a self-hosted deployment reads from; a
|
|
18
|
+
* staging Plitzi is the exception and says so.
|
|
19
|
+
*/
|
|
20
|
+
var PLITZI_SERVER_URL = "https://server.plitzi.com";
|
|
21
|
+
/**
|
|
22
|
+
* The one query. Written out rather than imported from the shared package's `gql` document because this runs in a
|
|
23
|
+
* server with no Apollo in it: the wire format is a string, and a string is what a `fetch` needs.
|
|
24
|
+
*/
|
|
25
|
+
var SPACE_QUERY = `query InitQuery($environment: String!, $revision: Int) {
|
|
26
|
+
Space(environment: $environment, revision: $revision) {
|
|
27
|
+
schema {
|
|
28
|
+
settings
|
|
29
|
+
flat { id definition { label type initialState styleSelectors bindings interactions parentId rootId items } attributes }
|
|
30
|
+
pages
|
|
31
|
+
pageFolders { id name slug parentId }
|
|
32
|
+
variables { name type value subValues { when value } }
|
|
33
|
+
}
|
|
34
|
+
segments {
|
|
35
|
+
id
|
|
36
|
+
identifier
|
|
37
|
+
definition
|
|
38
|
+
schema {
|
|
39
|
+
settings
|
|
40
|
+
variables { name type value subValues { value when } }
|
|
41
|
+
flat { id definition { label type initialState styleSelectors bindings interactions parentId rootId items } attributes }
|
|
42
|
+
}
|
|
43
|
+
style { cache }
|
|
44
|
+
}
|
|
45
|
+
plugins { type resource settings }
|
|
46
|
+
style { variables cache }
|
|
47
|
+
}
|
|
48
|
+
}`;
|
|
49
|
+
/**
|
|
50
|
+
* Which revision of an environment is current.
|
|
51
|
+
*
|
|
52
|
+
* A separate, tiny query on purpose: in latest mode this is what runs on a timer, and asking it is orders of
|
|
53
|
+
* magnitude cheaper — for this deployment and for Plitzi — than pulling a whole space down to discover it did not
|
|
54
|
+
* change. The space itself is fetched only when this answer moves.
|
|
55
|
+
*/
|
|
56
|
+
var LATEST_REVISION_QUERY = `query SpaceLatestRevisionQuery($environment: String!) {
|
|
57
|
+
SpaceLatestRevision(environment: $environment) {
|
|
58
|
+
snapshot { revision }
|
|
59
|
+
}
|
|
60
|
+
}`;
|
|
61
|
+
/**
|
|
62
|
+
* GraphQL answers `flat` as a LIST; every reader of a schema indexes it BY ID (`schema.flat[pageId]`, and the same
|
|
63
|
+
* for `parentId`/`rootId`/`items`). The two are the `SchemaRaw` and `Schema` types, and the conversion is the wire
|
|
64
|
+
* format's whole difference from the runtime one.
|
|
65
|
+
*
|
|
66
|
+
* Without it nothing throws — an array is a perfectly good object — it just answers `undefined` to every lookup, so
|
|
67
|
+
* the page router matches no page and every URL is a 404 on a space that fetched and parsed correctly.
|
|
68
|
+
*/
|
|
69
|
+
var byElementId = (flat) => {
|
|
70
|
+
if (!Array.isArray(flat)) return flat ?? {};
|
|
71
|
+
return flat.reduce((acum, element) => {
|
|
72
|
+
if (element.id) acum[element.id] = element;
|
|
73
|
+
return acum;
|
|
74
|
+
}, {});
|
|
75
|
+
};
|
|
76
|
+
/** The runtime shape of a schema: the wire's `flat` list, keyed. */
|
|
77
|
+
var toSchema = (schema) => ({
|
|
78
|
+
...schema,
|
|
79
|
+
flat: byElementId(schema.flat)
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Segments arrive as a list and are read by identifier, which is how every other reader of a space holds them. Each
|
|
83
|
+
* one carries a schema of its own, so its `flat` needs the same keying the space's does.
|
|
84
|
+
*/
|
|
85
|
+
var byIdentifier = (segments) => (segments ?? []).reduce((acum, segment) => {
|
|
86
|
+
const entry = segment;
|
|
87
|
+
const identifier = entry.identifier;
|
|
88
|
+
if (!identifier || identifier in acum) return acum;
|
|
89
|
+
return {
|
|
90
|
+
...acum,
|
|
91
|
+
[identifier]: {
|
|
92
|
+
...entry,
|
|
93
|
+
...entry.schema && { schema: toSchema(entry.schema) }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, {});
|
|
97
|
+
/**
|
|
98
|
+
* The `scope` claim, read without verifying anything.
|
|
99
|
+
*
|
|
100
|
+
* Verification is the server's and this cannot do it — there is no signing key here, and a key this deployment does
|
|
101
|
+
* not hold could not be checked anyway. Reading the claim is enough for what this is for: telling the operator, at
|
|
102
|
+
* startup, that they pasted the wrong one of their two keys.
|
|
103
|
+
*/
|
|
104
|
+
var scopeOf = (token) => {
|
|
105
|
+
const payload = token.split(".")[1];
|
|
106
|
+
if (!payload) return;
|
|
107
|
+
try {
|
|
108
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
109
|
+
return typeof claims.scope === "string" ? claims.scope : void 0;
|
|
110
|
+
} catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Refuse the PUBLIC key here, where the mistake is cheap, rather than as a 401 an hour later.
|
|
116
|
+
*
|
|
117
|
+
* The two keys are not interchangeable and the difference is the whole security model. A `render` key is embedded in
|
|
118
|
+
* every published page, so anyone who views source has it; what keeps a copied one from working is that a browser is
|
|
119
|
+
* made to state the origin it is presenting from, and that statement is checked. A server has no such statement to
|
|
120
|
+
* make — which is exactly why a server must not be reading with the public key: if it could, a render key lifted
|
|
121
|
+
* from someone else's site would be enough to serve a byte-identical clone of it from here.
|
|
122
|
+
*
|
|
123
|
+
* So self-hosting has a credential of its own. It is secret, issued once, bound to no domain, and revocable on its
|
|
124
|
+
* own row without touching the published site's key.
|
|
125
|
+
*/
|
|
126
|
+
var assertHostKey = (webKey) => {
|
|
127
|
+
const scope = scopeOf(webKey);
|
|
128
|
+
if (scope === void 0 || scope === "space:host") return;
|
|
129
|
+
if (scope === "space:render") throw new Error("createCloudAdapters was given the space’s PUBLIC render key. That key is embedded in published pages and is only honoured from the origins it declares, so a server cannot present it. Issue a host key instead (Credentials → “Self-hosting” in the builder, or POST /spaces/{id}/tokens/host) and keep it secret — it is not safe to commit or to ship in a page.");
|
|
130
|
+
throw new Error(`createCloudAdapters needs a space host key; this one is scoped "${scope}".`);
|
|
131
|
+
};
|
|
132
|
+
var createCloudAdapters = (config) => {
|
|
133
|
+
const { serverUrl = PLITZI_SERVER_URL, webKey, environment = "main", revision, cacheSeconds = 60, cache, spaceId = 1, deployment, fetchImpl = fetch } = config;
|
|
134
|
+
assertHostKey(webKey);
|
|
135
|
+
/** `main` is the document somebody is editing right now. Nothing about it is worth remembering for a minute. */
|
|
136
|
+
const isLive = (env) => env === "main";
|
|
137
|
+
/** A version somebody named: fetched once, kept for good, because a published revision cannot change. */
|
|
138
|
+
const isPinned = (env, rev) => !isLive(env) && typeof rev === "number" && rev > 0;
|
|
139
|
+
const post = async (query, variables, what) => {
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetchImpl(serverUrl, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: {
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
authorization: `Bearer ${webKey}`
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
query,
|
|
149
|
+
variables
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
console.error(`[CloudAdapters] ${what} answered ${response.status}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const payload = await response.json();
|
|
157
|
+
if (payload.errors?.length) {
|
|
158
|
+
console.error(`[CloudAdapters] ${what}: ${payload.errors.map((error) => error.message).join("; ")}`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
return payload.data;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`[CloudAdapters] ${what} could not be reached:`, error.message);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const fetchSpace = async (env, rev) => {
|
|
168
|
+
const space = (await post(SPACE_QUERY, {
|
|
169
|
+
environment: env,
|
|
170
|
+
revision: rev ?? null
|
|
171
|
+
}, `space ${env}@${rev ?? "latest"}`))?.Space;
|
|
172
|
+
if (!space?.schema) return;
|
|
173
|
+
return {
|
|
174
|
+
schema: toSchema(space.schema),
|
|
175
|
+
style: space.style,
|
|
176
|
+
plugins: space.plugins,
|
|
177
|
+
segments: byIdentifier(space.segments)
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const fetchLatestRevision = async (env) => {
|
|
181
|
+
return (await post(LATEST_REVISION_QUERY, { environment: env }, `latest revision of ${env}`))?.SpaceLatestRevision?.snapshot?.revision;
|
|
182
|
+
};
|
|
183
|
+
const cacheKey = (env, rev) => `plitzi:space:${env}:${rev}`;
|
|
184
|
+
const held = /* @__PURE__ */ new Map();
|
|
185
|
+
/** One request per key at a time: a burst of renders on a cold cache joins the first, it does not multiply it. */
|
|
186
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
187
|
+
const once = (key, run) => {
|
|
188
|
+
const pending = inFlight.get(key);
|
|
189
|
+
if (pending) return pending;
|
|
190
|
+
const request = run().finally(() => inFlight.delete(key));
|
|
191
|
+
inFlight.set(key, request);
|
|
192
|
+
return request;
|
|
193
|
+
};
|
|
194
|
+
/** Reads a published revision through the shared cache, then the network, and writes back what it learned. */
|
|
195
|
+
const loadRevision = (env, rev) => {
|
|
196
|
+
const key = cacheKey(env, rev);
|
|
197
|
+
return once(key, async () => {
|
|
198
|
+
try {
|
|
199
|
+
const stored = await cache?.get(key);
|
|
200
|
+
if (stored) {
|
|
201
|
+
const data = JSON.parse(stored);
|
|
202
|
+
held.set(env, {
|
|
203
|
+
revision: rev,
|
|
204
|
+
data,
|
|
205
|
+
checkedAt: Date.now()
|
|
206
|
+
});
|
|
207
|
+
return data;
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
const fetched = await fetchSpace(env, rev);
|
|
211
|
+
if (!fetched) return;
|
|
212
|
+
held.set(env, {
|
|
213
|
+
revision: rev,
|
|
214
|
+
data: fetched,
|
|
215
|
+
checkedAt: Date.now()
|
|
216
|
+
});
|
|
217
|
+
await cache?.set(key, JSON.stringify(fetched)).catch(() => void 0);
|
|
218
|
+
return fetched;
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Latest mode: find out which revision is current, and fetch the space only if that moved.
|
|
223
|
+
*
|
|
224
|
+
* The probe is the thing on the timer, not the space. A deployment that publishes once a week asks Plitzi for a
|
|
225
|
+
* revision number every minute and for a space once — and a publish is live within one window rather than
|
|
226
|
+
* whenever a blanket TTL happened to fall.
|
|
227
|
+
*
|
|
228
|
+
* A probe that fails leaves `checkedAt` alone so the next request tries again, and leaves the held copy serving.
|
|
229
|
+
*/
|
|
230
|
+
const refreshLatest = async (env) => {
|
|
231
|
+
const current = await fetchLatestRevision(env);
|
|
232
|
+
const previous = held.get(env);
|
|
233
|
+
if (current === void 0) return previous?.data;
|
|
234
|
+
if (previous && previous.revision === current) {
|
|
235
|
+
previous.checkedAt = Date.now();
|
|
236
|
+
return previous.data;
|
|
237
|
+
}
|
|
238
|
+
return await loadRevision(env, current) ?? previous?.data;
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* The space, and how hard this deployment leans on Plitzi to get it.
|
|
242
|
+
*
|
|
243
|
+
* - **`main`**: read live, every time. It is being edited; a cached answer is a wrong one.
|
|
244
|
+
* - **A pinned revision**: from cache forever after the first read. It cannot change.
|
|
245
|
+
* - **Latest**: the held copy is served immediately and a revision probe runs behind it once the window is up.
|
|
246
|
+
* After the first render no page ever waits on Plitzi, and a failed probe or fetch changes nothing — the last
|
|
247
|
+
* good copy keeps serving, because a self-hosted site going blank over somebody else's bad minute is not a
|
|
248
|
+
* trade worth making.
|
|
249
|
+
*/
|
|
250
|
+
const getOfflineData = async (_spaceId, env, rev) => {
|
|
251
|
+
if (isLive(env)) return once(`live:${env}`, () => fetchSpace(env));
|
|
252
|
+
const pinned = rev ?? revision;
|
|
253
|
+
if (pinned !== void 0 && isPinned(env, pinned)) {
|
|
254
|
+
const already = held.get(env);
|
|
255
|
+
return already?.revision === pinned ? already.data : loadRevision(env, pinned);
|
|
256
|
+
}
|
|
257
|
+
const current = held.get(env);
|
|
258
|
+
if (!current) return refreshLatest(env);
|
|
259
|
+
if (Date.now() - current.checkedAt >= cacheSeconds * 1e3) {
|
|
260
|
+
current.checkedAt = Date.now();
|
|
261
|
+
refreshLatest(env).catch(() => void 0);
|
|
262
|
+
}
|
|
263
|
+
return current.data;
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Every request is this one space.
|
|
267
|
+
*
|
|
268
|
+
* There is no host-to-space mapping to make: the key names the space, so a deployment that serves two of them
|
|
269
|
+
* builds two servers or supplies its own resolver — which is more honest than a lookup table that silently
|
|
270
|
+
* serves the wrong space when a domain is added and this is not.
|
|
271
|
+
*
|
|
272
|
+
* The revision it reports is the one actually being served, which in latest mode is whatever the last probe
|
|
273
|
+
* found: a deployment reading `at: { environment, revision }` must be told the version its page was built from,
|
|
274
|
+
* not the zero that means "whatever is live".
|
|
275
|
+
*/
|
|
276
|
+
const getSpaceDeployment = async () => {
|
|
277
|
+
if (isLive(environment)) return {
|
|
278
|
+
...deployment,
|
|
279
|
+
spaceId,
|
|
280
|
+
environment,
|
|
281
|
+
revision: 0
|
|
282
|
+
};
|
|
283
|
+
if (isPinned(environment, revision)) return {
|
|
284
|
+
...deployment,
|
|
285
|
+
spaceId,
|
|
286
|
+
environment,
|
|
287
|
+
revision
|
|
288
|
+
};
|
|
289
|
+
if (!held.has(environment)) await refreshLatest(environment);
|
|
290
|
+
return {
|
|
291
|
+
...deployment,
|
|
292
|
+
spaceId,
|
|
293
|
+
environment,
|
|
294
|
+
revision: held.get(environment)?.revision ?? 0
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
getOfflineData,
|
|
299
|
+
getSpaceDeployment
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
//#endregion
|
|
303
|
+
export { createCloudAdapters, createCloudAdapters as default };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { SpaceCache, SpaceResolution, SpaceResolver } from './types';
|
|
2
|
+
import { SSRRequest, SSRSpaceDeployment } from '@plitzi/sdk-shared';
|
|
3
|
+
export interface FrameAncestorsConfig {
|
|
4
|
+
/** The domains this space declared. Usually the origins on its public credential. */
|
|
5
|
+
find: (spaceId: number) => Promise<string[]>;
|
|
6
|
+
/** Always allowed on top of them — this deployment's own hosts, because a builder previews in an iframe. */
|
|
7
|
+
floor?: string[];
|
|
8
|
+
/** Cache the domain list too. Dropped by `invalidate.domains(spaceId)` when the owner edits it. */
|
|
9
|
+
cache?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SpaceAdaptersConfig {
|
|
12
|
+
/**
|
|
13
|
+
* How a request becomes a space, in order. The first resolver to answer wins, and one that REFUSES ends the
|
|
14
|
+
* chain — see {@link SpaceResolver}.
|
|
15
|
+
*
|
|
16
|
+
* Order is the only control here, and it is enough: put `authoringPreview` first so a credentialed request is
|
|
17
|
+
* never served as an anonymous visitor, and put a `fixedSpace` last if there should be no such thing as a host
|
|
18
|
+
* this server does not know.
|
|
19
|
+
*/
|
|
20
|
+
resolvers: SpaceResolver[];
|
|
21
|
+
cache?: SpaceCache;
|
|
22
|
+
/** Default 300. */
|
|
23
|
+
ttlSeconds?: number;
|
|
24
|
+
/**
|
|
25
|
+
* What a resolution is cached under, or `undefined` for a request that must not be cached at all.
|
|
26
|
+
*
|
|
27
|
+
* The default is the hostname, **except** for a request presenting a credential, which is not cached in either
|
|
28
|
+
* direction. The cache is shared and keyed by host, while a credential can resolve the same host to a different
|
|
29
|
+
* space — so a hit would serve one visitor's preview to the next, and a write would poison the host for everyone.
|
|
30
|
+
*/
|
|
31
|
+
cacheKey?: (req: SSRRequest) => string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Who may put this space in an iframe. Applied to every resolution, which is the point of it living here: a
|
|
34
|
+
* deployment that derives it per branch eventually forgets a branch, and the branch it forgets serves a space
|
|
35
|
+
* framable by anyone.
|
|
36
|
+
*/
|
|
37
|
+
frameAncestors?: FrameAncestorsConfig;
|
|
38
|
+
/**
|
|
39
|
+
* The last word, for whatever is this deployment's alone — which plugins a space loads, template props, a
|
|
40
|
+
* per-tenant theme. Runs on every resolution, after everything above.
|
|
41
|
+
*/
|
|
42
|
+
decorate?: (resolution: SpaceResolution, req: SSRRequest) => Promise<Partial<SpaceResolution>>;
|
|
43
|
+
/** What an unresolved request gets. Default `404 Space not found`. */
|
|
44
|
+
notFound?: {
|
|
45
|
+
code: number;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
/** Reported, never thrown: a resolution that fails is one bad request, not a dead server. */
|
|
49
|
+
onError?: (error: unknown, req?: SSRRequest) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `getSpaceDeployment`, assembled.
|
|
53
|
+
*
|
|
54
|
+
* Answering "which space is this request for" is the same shape everywhere and the same *rules* everywhere — try
|
|
55
|
+
* the ways a host can name a space, in order; never serve a credentialed request from a shared cache; derive the
|
|
56
|
+
* framing policy from what the space declared; say 404 rather than leaking whether a space exists. What differs
|
|
57
|
+
* between deployments is only where the rows are, which is what a resolver is.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const spaces = createSpaceAdapters({
|
|
61
|
+
* resolvers: [
|
|
62
|
+
* authoringPreview({ hosts, resolveGrant: auth.identity.resolveGrant, find: findSpace }),
|
|
63
|
+
* wildcardSubdomain({ suffix: 'example.app', find: findSpaceBySlug }),
|
|
64
|
+
* verifiedDomain(findDeploymentByDomain)
|
|
65
|
+
* ],
|
|
66
|
+
* cache: redisCache,
|
|
67
|
+
* frameAncestors: { find: findSpaceDomains, floor: hosts, cache: true }
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* createServer({ adapters: { ...spaces.adapters, getOfflineData } });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare const createSpaceAdapters: (config: SpaceAdaptersConfig) => {
|
|
74
|
+
/** Spread into `createServer({ adapters })` beside whatever answers `getOfflineData`. */
|
|
75
|
+
adapters: {
|
|
76
|
+
getSpaceDeployment: (req: SSRRequest) => Promise<SSRSpaceDeployment>;
|
|
77
|
+
};
|
|
78
|
+
getSpaceDeployment: (req: SSRRequest) => Promise<SSRSpaceDeployment>;
|
|
79
|
+
/**
|
|
80
|
+
* Drop what a change made untrue. Both matter the moment an owner edits something through an API: without the
|
|
81
|
+
* first a domain keeps resolving to the space it used to, and without the second a space stays framable by a
|
|
82
|
+
* site whose permission was just withdrawn — in both cases until the TTL, which is not a security boundary.
|
|
83
|
+
*/
|
|
84
|
+
invalidate: {
|
|
85
|
+
resolution: (key: string) => Promise<void>;
|
|
86
|
+
domains: (spaceId: number) => Promise<void>;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
export type SpaceAdapters = ReturnType<typeof createSpaceAdapters>;
|
|
90
|
+
export { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildcardSubdomain } from './resolvers';
|
|
91
|
+
export { createMemoryCache, refuse } from './types';
|
|
92
|
+
export type { AuthoringPreviewOptions, DeploymentRow, WildcardSubdomainOptions } from './resolvers';
|
|
93
|
+
export type { SpaceCache, SpaceRefusal, SpaceResolution, SpaceResolver } from './types';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createMemoryCache, isRefusal, refuse } from "./types.js";
|
|
2
|
+
import { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildcardSubdomain } from "./resolvers.js";
|
|
3
|
+
import { frameAncestors } from "../../core/auth/domains.js";
|
|
4
|
+
//#region src/adapters/space/index.ts
|
|
5
|
+
var RESOLUTION_PREFIX = "space:resolution:";
|
|
6
|
+
var DOMAINS_PREFIX = "space:domains:";
|
|
7
|
+
/**
|
|
8
|
+
* `getSpaceDeployment`, assembled.
|
|
9
|
+
*
|
|
10
|
+
* Answering "which space is this request for" is the same shape everywhere and the same *rules* everywhere — try
|
|
11
|
+
* the ways a host can name a space, in order; never serve a credentialed request from a shared cache; derive the
|
|
12
|
+
* framing policy from what the space declared; say 404 rather than leaking whether a space exists. What differs
|
|
13
|
+
* between deployments is only where the rows are, which is what a resolver is.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const spaces = createSpaceAdapters({
|
|
17
|
+
* resolvers: [
|
|
18
|
+
* authoringPreview({ hosts, resolveGrant: auth.identity.resolveGrant, find: findSpace }),
|
|
19
|
+
* wildcardSubdomain({ suffix: 'example.app', find: findSpaceBySlug }),
|
|
20
|
+
* verifiedDomain(findDeploymentByDomain)
|
|
21
|
+
* ],
|
|
22
|
+
* cache: redisCache,
|
|
23
|
+
* frameAncestors: { find: findSpaceDomains, floor: hosts, cache: true }
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* createServer({ adapters: { ...spaces.adapters, getOfflineData } });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
var createSpaceAdapters = (config) => {
|
|
30
|
+
const { resolvers, cache, ttlSeconds = 300, cacheKey = (req) => presentsCredential(req) ? void 0 : req.hostname, frameAncestors: frameAncestors$1, decorate, notFound = {
|
|
31
|
+
code: 404,
|
|
32
|
+
message: "Space not found"
|
|
33
|
+
}, onError } = config;
|
|
34
|
+
const readCache = async (key) => {
|
|
35
|
+
if (!cache) return;
|
|
36
|
+
try {
|
|
37
|
+
const raw = await cache.get(key);
|
|
38
|
+
return raw === void 0 ? void 0 : JSON.parse(raw);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
onError?.(error);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const writeCache = async (key, value) => {
|
|
45
|
+
if (!cache) return;
|
|
46
|
+
try {
|
|
47
|
+
await cache.set(key, JSON.stringify(value), ttlSeconds);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
onError?.(error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const domainsOf = async (spaceId) => {
|
|
53
|
+
if (!frameAncestors$1) return [];
|
|
54
|
+
const key = `${DOMAINS_PREFIX}${spaceId}`;
|
|
55
|
+
if (frameAncestors$1.cache) {
|
|
56
|
+
const cached = await readCache(key);
|
|
57
|
+
if (cached) return cached;
|
|
58
|
+
}
|
|
59
|
+
const domains = await frameAncestors$1.find(spaceId);
|
|
60
|
+
if (frameAncestors$1.cache) await writeCache(key, domains);
|
|
61
|
+
return domains;
|
|
62
|
+
};
|
|
63
|
+
/** Everything that is true of a resolution regardless of which resolver produced it. */
|
|
64
|
+
const complete = async (resolution, req) => {
|
|
65
|
+
const decorated = decorate ? {
|
|
66
|
+
...resolution,
|
|
67
|
+
...await decorate(resolution, req)
|
|
68
|
+
} : resolution;
|
|
69
|
+
return {
|
|
70
|
+
environment: "main",
|
|
71
|
+
revision: 0,
|
|
72
|
+
...decorated,
|
|
73
|
+
...frameAncestors$1 ? { frameAncestors: frameAncestors(await domainsOf(decorated.spaceId), frameAncestors$1.floor ?? []) } : {}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const runResolvers = async (req) => {
|
|
77
|
+
for (const resolve of resolvers) {
|
|
78
|
+
const answer = await resolve(req);
|
|
79
|
+
if (answer) return answer;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const getSpaceDeployment = async (req) => {
|
|
83
|
+
const key = cacheKey(req);
|
|
84
|
+
try {
|
|
85
|
+
if (key !== void 0) {
|
|
86
|
+
const cached = await readCache(`${RESOLUTION_PREFIX}${key}`);
|
|
87
|
+
if (cached) return await complete(cached, req);
|
|
88
|
+
}
|
|
89
|
+
const answer = await runResolvers(req);
|
|
90
|
+
if (!answer) return { error: notFound };
|
|
91
|
+
if (isRefusal(answer)) return { error: answer.refuse };
|
|
92
|
+
if (key !== void 0 && !answer.authoring) await writeCache(`${RESOLUTION_PREFIX}${key}`, answer);
|
|
93
|
+
return await complete(answer, req);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
onError?.(error, req);
|
|
96
|
+
return { error: {
|
|
97
|
+
code: 500,
|
|
98
|
+
message: "Space could not be resolved"
|
|
99
|
+
} };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
/** Spread into `createServer({ adapters })` beside whatever answers `getOfflineData`. */
|
|
104
|
+
adapters: { getSpaceDeployment },
|
|
105
|
+
getSpaceDeployment,
|
|
106
|
+
/**
|
|
107
|
+
* Drop what a change made untrue. Both matter the moment an owner edits something through an API: without the
|
|
108
|
+
* first a domain keeps resolving to the space it used to, and without the second a space stays framable by a
|
|
109
|
+
* site whose permission was just withdrawn — in both cases until the TTL, which is not a security boundary.
|
|
110
|
+
*/
|
|
111
|
+
invalidate: {
|
|
112
|
+
resolution: async (key) => {
|
|
113
|
+
await cache?.delete(`${RESOLUTION_PREFIX}${key}`);
|
|
114
|
+
},
|
|
115
|
+
domains: async (spaceId) => {
|
|
116
|
+
await cache?.delete(`${DOMAINS_PREFIX}${spaceId}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
export { authoringPreview, createMemoryCache, createSpaceAdapters, fixedSpace, presentsCredential, refuse, verifiedDomain, wildcardSubdomain };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SpaceResolution, SpaceResolver } from './types';
|
|
2
|
+
import { GrantOptions, GrantResult } from '../../core/auth/identity';
|
|
3
|
+
import { Environment, SSRCredential, SSRRequest } from '@plitzi/sdk-shared';
|
|
4
|
+
/**
|
|
5
|
+
* Did this request even try to present a credential?
|
|
6
|
+
*
|
|
7
|
+
* Not "is the credential good" — that is `resolveGrant`'s answer, and asking it of every anonymous visitor who
|
|
8
|
+
* lands on a first-party host would turn them all into 403s. This is the gate before it, and it is also what marks
|
|
9
|
+
* a request as un-cacheable: the shared resolution cache is keyed by host, and a request carrying a credential can
|
|
10
|
+
* resolve to a different space than the same host without one.
|
|
11
|
+
*/
|
|
12
|
+
export declare const presentsCredential: (req: SSRRequest) => boolean;
|
|
13
|
+
/** What a deployment row has to be able to say. Anything else on your row stays yours. */
|
|
14
|
+
export interface DeploymentRow {
|
|
15
|
+
spaceId: number;
|
|
16
|
+
environment?: Environment;
|
|
17
|
+
revision?: number | null;
|
|
18
|
+
credential?: SSRCredential;
|
|
19
|
+
/** `false` refuses the request outright. A domain that is not proven is not a domain this space answers on. */
|
|
20
|
+
verified?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The ordinary case: a custom domain someone pointed at this server, resolved through a row that says it was
|
|
24
|
+
* verified. Returns nothing when the domain is unknown, so a later resolver still gets a turn.
|
|
25
|
+
*/
|
|
26
|
+
export declare const verifiedDomain: (find: (domain: string) => Promise<DeploymentRow | undefined>) => SpaceResolver;
|
|
27
|
+
export interface WildcardSubdomainOptions {
|
|
28
|
+
/** The domain the sub-domains hang off, with or without a leading dot — `plitzi.app` or `.plitzi.app`. */
|
|
29
|
+
suffix: string;
|
|
30
|
+
/** The slug IS the sub-domain. Return nothing for a slug that names no space. */
|
|
31
|
+
find: (slug: string) => Promise<{
|
|
32
|
+
spaceId: number;
|
|
33
|
+
environment?: Environment;
|
|
34
|
+
} | undefined>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every space reachable at `<slug>.example.com` without anybody configuring a domain for it.
|
|
38
|
+
*
|
|
39
|
+
* The point is that it needs no per-space row: the sub-domain is the identifier, so a space is publishable the
|
|
40
|
+
* moment it exists. It always resolves the live snapshot — a free preview domain that served a pinned revision
|
|
41
|
+
* would be a second thing to deploy to.
|
|
42
|
+
*/
|
|
43
|
+
export declare const wildcardSubdomain: ({ suffix, find }: WildcardSubdomainOptions) => SpaceResolver;
|
|
44
|
+
export interface AuthoringPreviewOptions {
|
|
45
|
+
/** The hosts this deployment owns. A credential presented anywhere else is not an authoring session. */
|
|
46
|
+
hosts: string[];
|
|
47
|
+
/** Usually `auth.identity.resolveGrant`. */
|
|
48
|
+
resolveGrant: (req: SSRRequest, options: GrantOptions) => Promise<GrantResult>;
|
|
49
|
+
/** Confirms the space exists and is readable. Return nothing and the request is a 404. */
|
|
50
|
+
find: (spaceId: number) => Promise<{
|
|
51
|
+
spaceId: number;
|
|
52
|
+
environment?: Environment;
|
|
53
|
+
} | undefined>;
|
|
54
|
+
/** Overrides what counts as "tried to present a credential". Defaults to {@link presentsCredential}. */
|
|
55
|
+
presented?: (req: SSRRequest) => boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* An author looking at their own space through a builder, rather than a visitor being served.
|
|
59
|
+
*
|
|
60
|
+
* Put this FIRST. It engages only when a credential is presented on a host this deployment owns, and once it has,
|
|
61
|
+
* a bad credential is a refusal rather than a fall-through — a request that tried to act for a space and failed
|
|
62
|
+
* must not then be served as an anonymous visitor of whatever else that host resolves to.
|
|
63
|
+
*
|
|
64
|
+
* `skipOrigin` is deliberate: a preview is loaded as a document, and a top-level navigation carries no `Origin`.
|
|
65
|
+
* It does not skip the domain binding, which is the part that matters.
|
|
66
|
+
*/
|
|
67
|
+
export declare const authoringPreview: ({ hosts, resolveGrant, find, presented }: AuthoringPreviewOptions) => SpaceResolver;
|
|
68
|
+
/** A space this server always serves, whatever the host — a single-space deployment, or the last word in a chain. */
|
|
69
|
+
export declare const fixedSpace: (resolution: SpaceResolution) => SpaceResolver;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { refuse } from "./types.js";
|
|
2
|
+
//#region src/adapters/space/resolvers.ts
|
|
3
|
+
/**
|
|
4
|
+
* Did this request even try to present a credential?
|
|
5
|
+
*
|
|
6
|
+
* Not "is the credential good" — that is `resolveGrant`'s answer, and asking it of every anonymous visitor who
|
|
7
|
+
* lands on a first-party host would turn them all into 403s. This is the gate before it, and it is also what marks
|
|
8
|
+
* a request as un-cacheable: the shared resolution cache is keyed by host, and a request carrying a credential can
|
|
9
|
+
* resolve to a different space than the same host without one.
|
|
10
|
+
*/
|
|
11
|
+
var presentsCredential = (req) => Boolean(req.query["access-token"]) || Boolean(req.headers.authorization);
|
|
12
|
+
/**
|
|
13
|
+
* The ordinary case: a custom domain someone pointed at this server, resolved through a row that says it was
|
|
14
|
+
* verified. Returns nothing when the domain is unknown, so a later resolver still gets a turn.
|
|
15
|
+
*/
|
|
16
|
+
var verifiedDomain = (find) => {
|
|
17
|
+
return async (req) => {
|
|
18
|
+
const row = await find(req.hostname);
|
|
19
|
+
if (!row) return;
|
|
20
|
+
if (row.verified === false) return refuse(404, "Domain not verified");
|
|
21
|
+
const { spaceId, environment, credential } = row;
|
|
22
|
+
return {
|
|
23
|
+
spaceId,
|
|
24
|
+
...environment ? { environment } : {},
|
|
25
|
+
revision: row.revision ?? 0,
|
|
26
|
+
...credential ? { credential } : {}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Every space reachable at `<slug>.example.com` without anybody configuring a domain for it.
|
|
32
|
+
*
|
|
33
|
+
* The point is that it needs no per-space row: the sub-domain is the identifier, so a space is publishable the
|
|
34
|
+
* moment it exists. It always resolves the live snapshot — a free preview domain that served a pinned revision
|
|
35
|
+
* would be a second thing to deploy to.
|
|
36
|
+
*/
|
|
37
|
+
var wildcardSubdomain = ({ suffix, find }) => {
|
|
38
|
+
const dotted = suffix.startsWith(".") ? suffix : `.${suffix}`;
|
|
39
|
+
return async (req) => {
|
|
40
|
+
if (!req.hostname.endsWith(dotted)) return;
|
|
41
|
+
const slug = req.hostname.slice(0, -dotted.length);
|
|
42
|
+
if (!slug || slug.includes(".")) return;
|
|
43
|
+
const space = await find(slug);
|
|
44
|
+
return space ? {
|
|
45
|
+
...space,
|
|
46
|
+
revision: 0
|
|
47
|
+
} : void 0;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* An author looking at their own space through a builder, rather than a visitor being served.
|
|
52
|
+
*
|
|
53
|
+
* Put this FIRST. It engages only when a credential is presented on a host this deployment owns, and once it has,
|
|
54
|
+
* a bad credential is a refusal rather than a fall-through — a request that tried to act for a space and failed
|
|
55
|
+
* must not then be served as an anonymous visitor of whatever else that host resolves to.
|
|
56
|
+
*
|
|
57
|
+
* `skipOrigin` is deliberate: a preview is loaded as a document, and a top-level navigation carries no `Origin`.
|
|
58
|
+
* It does not skip the domain binding, which is the part that matters.
|
|
59
|
+
*/
|
|
60
|
+
var authoringPreview = ({ hosts, resolveGrant, find, presented = presentsCredential }) => {
|
|
61
|
+
return async (req) => {
|
|
62
|
+
if (!hosts.includes(req.hostname) || !presented(req)) return;
|
|
63
|
+
const result = await resolveGrant(req, { skipOrigin: true });
|
|
64
|
+
if (!result.ok) return refuse(403, "Access Not Authorized");
|
|
65
|
+
const space = await find(result.grant.spaceId);
|
|
66
|
+
if (!space) return refuse(404, "Space not found");
|
|
67
|
+
return {
|
|
68
|
+
...space,
|
|
69
|
+
revision: 0,
|
|
70
|
+
authoring: true
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** A space this server always serves, whatever the host — a single-space deployment, or the last word in a chain. */
|
|
75
|
+
var fixedSpace = (resolution) => {
|
|
76
|
+
return () => Promise.resolve(resolution);
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
export { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildcardSubdomain };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|