@softr/sdk 0.0.22-alpha.1 → 0.0.22

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.
@@ -0,0 +1,121 @@
1
+ import { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName } from "../auth/session.js";
2
+
3
+ //#region src/preview-identity/preview-identity.ts
4
+ const PREVIEW_REDEMPTION_PATH = "/__preview/identity";
5
+ const PREVIEW_SIGNED_IN_PATH = "/__preview/signed-in";
6
+ /** The grant in the URL is the only credential: studio-api authorized the builder before issuing it. */
7
+ const redeemGrant = async (req, res) => {
8
+ res.setHeader("Cache-Control", "no-store");
9
+ res.setHeader("Referrer-Policy", "no-referrer");
10
+ if (req.method !== "GET") {
11
+ sendJson(res, 405, { error: `${PREVIEW_REDEMPTION_PATH} only supports GET` });
12
+ return;
13
+ }
14
+ const grant = searchParamsOf(req).get("grant");
15
+ if (!grant) {
16
+ sendJson(res, 400, { error: "no grant" });
17
+ return;
18
+ }
19
+ const landing = landingPath(searchParamsOf(req).get("next"));
20
+ let session;
21
+ try {
22
+ session = await redeemPreviewGrant(req, grant);
23
+ } catch (error) {
24
+ sendJson(res, statusOf(error), { error: messageOf(error) });
25
+ return;
26
+ }
27
+ const secure = requestIsHttps(req);
28
+ res.statusCode = 302;
29
+ res.setHeader("Location", landing);
30
+ res.setHeader("Set-Cookie", session.anonymous ? makeSessionClearingCookies(secure) : makeSessionTokenCookie(session.jwt, secure));
31
+ res.end();
32
+ };
33
+ /** Presence only, never decoded: decoding would put an app user's identity on a route Studio reads. */
34
+ const reportSignedIn = (req, res) => {
35
+ res.setHeader("Cache-Control", "no-store");
36
+ if (req.method !== "GET") {
37
+ sendJson(res, 405, { error: `${PREVIEW_SIGNED_IN_PATH} only supports GET` });
38
+ return;
39
+ }
40
+ sendJson(res, 200, { signedIn: hasSessionCookie(req.headers.cookie, sessionTokenCookieName(requestIsHttps(req))) });
41
+ };
42
+ var RedemptionError = class extends Error {
43
+ status;
44
+ constructor(status, message) {
45
+ super(message);
46
+ this.status = status;
47
+ }
48
+ };
49
+ function statusOf(error) {
50
+ return error instanceof RedemptionError ? error.status : 503;
51
+ }
52
+ function messageOf(error) {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
55
+ async function redeemPreviewGrant(req, grant) {
56
+ const applicationId = process.env.SOFTR_APPLICATION_ID;
57
+ const studioApiUrl = process.env.SOFTR_STUDIO_API_URL?.trim() || studioApiUrlFor(req);
58
+ if (!applicationId || !studioApiUrl) throw new RedemptionError(503, "studio-api is unavailable");
59
+ let response;
60
+ try {
61
+ response = await fetch(`${studioApiUrl}/v1/applications/${encodeURIComponent(applicationId)}/vibe-coding/preview-identity/redeem`, {
62
+ method: "POST",
63
+ headers: { "content-type": "application/json" },
64
+ body: JSON.stringify({ grant })
65
+ });
66
+ } catch {
67
+ throw new RedemptionError(503, "studio-api is unreachable");
68
+ }
69
+ if (!response.ok) throw new RedemptionError(response.status, "studio-api refused the preview identity grant");
70
+ const body = await response.json().catch(() => null);
71
+ if (typeof body !== "object" || body === null) throw new RedemptionError(503, "studio-api returned no session");
72
+ const { anonymous, jwt } = body;
73
+ if (anonymous === true) return { anonymous: true };
74
+ if (typeof jwt !== "string" || jwt.length === 0) throw new RedemptionError(503, "studio-api returned no token");
75
+ return {
76
+ anonymous: false,
77
+ jwt
78
+ };
79
+ }
80
+ const STAGING_HOST_PATTERN = /(?:^|\.)staging(\d+)\.softr\.io$/;
81
+ /** The host is the only per-environment signal the app has. Anything but a staging one gives null. */
82
+ function studioApiUrlFor(req) {
83
+ const forwarded = req.headers["x-forwarded-host"];
84
+ const name = ((Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host ?? "").split(",")[0].trim().split(":")[0];
85
+ const digits = STAGING_HOST_PATTERN.exec(name)?.[1];
86
+ if (!digits) return null;
87
+ return `https://studio-api-staging${digits.padStart(2, "0")}.softr.io`;
88
+ }
89
+ function hasSessionCookie(header, name) {
90
+ if (!header) return false;
91
+ return header.split(";").some((pair) => {
92
+ const separator = pair.indexOf("=");
93
+ return separator !== -1 && pair.slice(0, separator).trim() === name;
94
+ });
95
+ }
96
+ /**
97
+ * Exactly as strict as h3's `getRequestProtocol`, which reads the cookie back. Being more generous
98
+ * writes `__Host-session_token` where the app looks up `session_token`, and the visitor is silently
99
+ * anonymous with no error on either side.
100
+ */
101
+ function requestIsHttps(req) {
102
+ const forwarded = req.headers["x-forwarded-proto"];
103
+ if (forwarded === "https") return true;
104
+ if (forwarded === "http") return false;
105
+ return Boolean(req.socket?.encrypted);
106
+ }
107
+ function landingPath(next) {
108
+ if (!next || !next.startsWith("/") || next.startsWith("//") || next.includes("\\") || next.includes("://") || /[\u0000-\u001f\u007f]/.test(next)) return "/";
109
+ return next;
110
+ }
111
+ function searchParamsOf(req) {
112
+ return new URL(req.url ?? "", "http://request.invalid").searchParams;
113
+ }
114
+ function sendJson(res, status, body) {
115
+ res.statusCode = status;
116
+ res.setHeader("Content-Type", "application/json");
117
+ res.end(JSON.stringify(body));
118
+ }
119
+
120
+ //#endregion
121
+ export { PREVIEW_REDEMPTION_PATH, PREVIEW_SIGNED_IN_PATH, redeemGrant, reportSignedIn };
package/dist/vite.js CHANGED
@@ -31,7 +31,29 @@ async function generateManifest(root) {
31
31
  * edit engine when the pod supplies one.
32
32
  */
33
33
  function softr() {
34
- return [routesManifest(), editEngine()];
34
+ return [
35
+ routesManifest(),
36
+ previewIdentity(),
37
+ editEngine()
38
+ ];
39
+ }
40
+ /**
41
+ * Signing the preview in writes the cookie this app's own session code reads back, so it lives here
42
+ * rather than in the edit engine — one implementation of the format, no version to keep in step.
43
+ *
44
+ * `SOFTR_APPLICATION_ID` gates it: the pod sets it, a developer's machine has none.
45
+ */
46
+ async function previewIdentity() {
47
+ if (!process.env.SOFTR_APPLICATION_ID) return false;
48
+ const { PREVIEW_REDEMPTION_PATH, PREVIEW_SIGNED_IN_PATH, redeemGrant, reportSignedIn } = await import("./preview-identity/preview-identity.js");
49
+ return {
50
+ name: "softr-preview-identity",
51
+ apply: "serve",
52
+ configureServer(server) {
53
+ server.middlewares.use(PREVIEW_REDEMPTION_PATH, redeemGrant);
54
+ server.middlewares.use(PREVIEW_SIGNED_IN_PATH, reportSignedIn);
55
+ }
56
+ };
35
57
  }
36
58
  /** Plugin order does not matter: this puts nothing into the module graph. */
37
59
  function routesManifest() {
@@ -61,21 +83,12 @@ async function editEngine() {
61
83
  if (!dir) return false;
62
84
  const entry = pathToFileURL(path.join(dir, "dist", "vite.js")).href;
63
85
  try {
64
- return (await import(entry)).editEngineStudio({ sessionCookies: await sessionCookies() });
86
+ return (await import(entry)).editEngineStudio();
65
87
  } catch (error) {
66
88
  console.error(`[softr] could not load the edit engine from ${entry}:`, error);
67
89
  return false;
68
90
  }
69
91
  }
70
- /** Imported only for a preview, so an ordinary build never loads the session machinery at all. */
71
- async function sessionCookies() {
72
- const { makeSessionTokenCookie, makeSessionClearingCookies, sessionTokenCookieName } = await import("./auth/session.js");
73
- return {
74
- makeSessionTokenCookie,
75
- makeSessionClearingCookies,
76
- sessionTokenCookieName
77
- };
78
- }
79
92
 
80
93
  //#endregion
81
94
  export { MANIFEST_RELATIVE_PATH, generateManifest, softr };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.22-alpha.1",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: core
12
12
  library: softr-sdk
13
- library_version: 0.0.22-alpha.1
13
+ library_version: 0.0.22
14
14
  ---
15
15
 
16
16
  # Softr SDK core
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.22-alpha.1
13
+ library_version: 0.0.22
14
14
  ---
15
15
 
16
16
  This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.22-alpha.1
13
+ library_version: 0.0.22
14
14
  ---
15
15
 
16
16
  This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.
@@ -8,7 +8,7 @@ description: >-
8
8
  metadata:
9
9
  type: sub-skill
10
10
  library: softr-sdk
11
- library_version: 0.0.22-alpha.1
11
+ library_version: 0.0.22
12
12
  ---
13
13
 
14
14
  This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.22-alpha.1
13
+ library_version: 0.0.22
14
14
  ---
15
15
 
16
16
  This skill builds on softr-sdk. Read it first for the entrypoints and the client/server boundary.
@@ -12,7 +12,7 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.22-alpha.1
15
+ library_version: 0.0.22
16
16
  ---
17
17
 
18
18
  This skill builds on softr-sdk. Read it first for the entrypoints and the wiring that puts a visitor on route context.