@farm.js/workos 0.1.0-beta.96 → 0.1.0-beta.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -22
- package/package.json +3 -3
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAuC,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAWhG,OAAO,KAAK,EAAuB,oBAAoB,EAAuB,MAAM,aAAa,CAAC;AAQlG,MAAM,WAAW,sBAAsB;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAc/C,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAqFD,wBAAgB,MAAM,CAAC,KAAK,GAAE,sBAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAqMG,MAAM;;;;yBAoE7B,OAAO;;GAe3C"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { WorkOS } from "@workos-inc/node";
|
|
2
3
|
import { defineIntegration, integrationRoute } from "@farm.js/core";
|
|
3
4
|
import { clearRequestCookie, createPathInferredClientApi, createDocumentNavigationMatchers, createRequestCookie, getCookieValue, getReturnTo, integrationConfig, normalizeMatchers, } from "@farm.js/integration-utils";
|
|
4
5
|
import { workosClient } from "./client.js";
|
|
5
6
|
const DEV_COOKIE_PASSWORD = "farmjs-workos-cookie-password-development-2026";
|
|
7
|
+
// The built server re-evaluates farm.config.ts at runtime to instantiate
|
|
8
|
+
// integrations, and Farm does not force NODE_ENV=production into that process.
|
|
9
|
+
// Treating an absent NODE_ENV as "development" would silently seal production
|
|
10
|
+
// sessions with the public DEV_COOKIE_PASSWORD, so only an explicit dev/test
|
|
11
|
+
// value may use it. `farm dev` runs through Vite, which sets
|
|
12
|
+
// NODE_ENV="development".
|
|
13
|
+
function isExplicitDevelopmentEnv() {
|
|
14
|
+
return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
15
|
+
}
|
|
6
16
|
function createWorkOSApi(input) {
|
|
7
17
|
return createPathInferredClientApi({
|
|
8
18
|
path: input.loginPath,
|
|
@@ -24,12 +34,14 @@ function resolveEnv(input) {
|
|
|
24
34
|
const cookiePassword = input.cookiePassword ??
|
|
25
35
|
process.env.WORKOS_COOKIE_PASSWORD ??
|
|
26
36
|
process.env.FARM_WORKOS_COOKIE_PASSWORD ??
|
|
27
|
-
(
|
|
37
|
+
(isExplicitDevelopmentEnv() ? DEV_COOKIE_PASSWORD : "");
|
|
28
38
|
if (!clientId || (!input.instance && !apiKey)) {
|
|
29
39
|
throw new Error("WorkOS integration requires a client ID and either a WorkOS instance or WORKOS_API_KEY.");
|
|
30
40
|
}
|
|
31
41
|
if (!cookiePassword) {
|
|
32
|
-
throw new Error("WorkOS integration requires WORKOS_COOKIE_PASSWORD
|
|
42
|
+
throw new Error("WorkOS integration requires WORKOS_COOKIE_PASSWORD. A development-only fallback is used " +
|
|
43
|
+
'only when NODE_ENV is "development" or "test"; set WORKOS_COOKIE_PASSWORD to a random ' +
|
|
44
|
+
"32-byte value in every other environment.");
|
|
33
45
|
}
|
|
34
46
|
return {
|
|
35
47
|
clientId,
|
|
@@ -69,19 +81,62 @@ export function workos(input = {}) {
|
|
|
69
81
|
apiKey,
|
|
70
82
|
clientId,
|
|
71
83
|
});
|
|
84
|
+
// The OAuth `state` must be unguessable and bound to the browser that started
|
|
85
|
+
// the flow. We keep a random nonce in an HMAC-signed, http-only cookie and
|
|
86
|
+
// require the callback's state to match it, so an attacker cannot feed a
|
|
87
|
+
// victim a code/state pair from their own login (login CSRF / code injection).
|
|
88
|
+
const stateCookieName = "farm_workos_state";
|
|
89
|
+
function signState(payload) {
|
|
90
|
+
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
91
|
+
const signature = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
|
|
92
|
+
return `${encoded}.${signature}`;
|
|
93
|
+
}
|
|
94
|
+
function unsignState(signed) {
|
|
95
|
+
if (!signed)
|
|
96
|
+
return null;
|
|
97
|
+
const separator = signed.lastIndexOf(".");
|
|
98
|
+
if (separator <= 0)
|
|
99
|
+
return null;
|
|
100
|
+
const encoded = signed.slice(0, separator);
|
|
101
|
+
const signature = signed.slice(separator + 1);
|
|
102
|
+
const expected = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
|
|
103
|
+
if (signature.length !== expected.length)
|
|
104
|
+
return null;
|
|
105
|
+
if (!timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
110
|
+
if (!parsed || typeof parsed !== "object")
|
|
111
|
+
return null;
|
|
112
|
+
const { state, returnTo } = parsed;
|
|
113
|
+
if (typeof state !== "string" || typeof returnTo !== "string")
|
|
114
|
+
return null;
|
|
115
|
+
return { state, returnTo };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
72
121
|
async function redirectToAuth(request, screenHint) {
|
|
73
122
|
const requestUrl = new URL(request.url);
|
|
74
123
|
const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
|
|
75
124
|
const callbackUrl = new URL(callbackPath, requestUrl.origin);
|
|
125
|
+
const state = randomBytes(16).toString("hex");
|
|
76
126
|
const authorizationUrl = workos.userManagement.getAuthorizationUrl({
|
|
77
127
|
provider: "authkit",
|
|
78
128
|
clientId,
|
|
79
129
|
redirectUri: callbackUrl.toString(),
|
|
80
130
|
screenHint,
|
|
81
|
-
state
|
|
131
|
+
state,
|
|
82
132
|
});
|
|
133
|
+
const headers = new Headers();
|
|
134
|
+
headers.append("set-cookie", createRequestCookie(stateCookieName, signState({ state, returnTo }), request, {
|
|
135
|
+
maxAge: 600,
|
|
136
|
+
}));
|
|
83
137
|
return {
|
|
84
|
-
redirectTo: authorizationUrl,
|
|
138
|
+
result: { redirectTo: authorizationUrl },
|
|
139
|
+
headers,
|
|
85
140
|
};
|
|
86
141
|
}
|
|
87
142
|
return defineIntegration({
|
|
@@ -124,21 +179,23 @@ export function workos(input = {}) {
|
|
|
124
179
|
integrationRoute.get(loginPath, {
|
|
125
180
|
responseFormat: "json",
|
|
126
181
|
async handler(request) {
|
|
127
|
-
const result = await redirectToAuth(request, "sign-in");
|
|
182
|
+
const { result, headers } = await redirectToAuth(request, "sign-in");
|
|
128
183
|
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
129
|
-
return Response.json(result);
|
|
184
|
+
return Response.json(result, { headers });
|
|
130
185
|
}
|
|
131
|
-
|
|
186
|
+
headers.set("location", result.redirectTo);
|
|
187
|
+
return new Response(null, { status: 302, headers });
|
|
132
188
|
},
|
|
133
189
|
}),
|
|
134
190
|
integrationRoute.get(signUpPath, {
|
|
135
191
|
responseFormat: "json",
|
|
136
192
|
async handler(request) {
|
|
137
|
-
const result = await redirectToAuth(request, "sign-up");
|
|
193
|
+
const { result, headers } = await redirectToAuth(request, "sign-up");
|
|
138
194
|
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
139
|
-
return Response.json(result);
|
|
195
|
+
return Response.json(result, { headers });
|
|
140
196
|
}
|
|
141
|
-
|
|
197
|
+
headers.set("location", result.redirectTo);
|
|
198
|
+
return new Response(null, { status: 302, headers });
|
|
142
199
|
},
|
|
143
200
|
}),
|
|
144
201
|
integrationRoute.get(callbackPath, {
|
|
@@ -153,6 +210,13 @@ export function workos(input = {}) {
|
|
|
153
210
|
if (!code) {
|
|
154
211
|
return new Response("Missing WorkOS authorization code.", { status: 400 });
|
|
155
212
|
}
|
|
213
|
+
// Bind the callback to the browser that started the flow. Verify before
|
|
214
|
+
// the code is exchanged so an injected code is never redeemed.
|
|
215
|
+
const statePayload = unsignState(getCookieValue(request.headers, stateCookieName));
|
|
216
|
+
const state = requestUrl.searchParams.get("state");
|
|
217
|
+
if (!statePayload || !state || statePayload.state !== state) {
|
|
218
|
+
return new Response("Invalid WorkOS authentication state.", { status: 400 });
|
|
219
|
+
}
|
|
156
220
|
const authentication = await workos.userManagement.authenticateWithCode({
|
|
157
221
|
clientId,
|
|
158
222
|
code,
|
|
@@ -164,19 +228,11 @@ export function workos(input = {}) {
|
|
|
164
228
|
if (!authentication.sealedSession) {
|
|
165
229
|
return new Response("WorkOS did not return a sealed session.", { status: 500 });
|
|
166
230
|
}
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
if (rawState) {
|
|
170
|
-
try {
|
|
171
|
-
const parsed = JSON.parse(rawState);
|
|
172
|
-
returnTo = getReturnTo(parsed.returnTo ?? null, "/dashboard");
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
returnTo = "/dashboard";
|
|
176
|
-
}
|
|
177
|
-
}
|
|
231
|
+
// Taken from the signed cookie, never from the untrusted query string.
|
|
232
|
+
const returnTo = getReturnTo(statePayload.returnTo, "/dashboard");
|
|
178
233
|
const headers = new Headers();
|
|
179
|
-
headers.
|
|
234
|
+
headers.append("set-cookie", createRequestCookie(cookieName, authentication.sealedSession, request));
|
|
235
|
+
headers.append("set-cookie", clearRequestCookie(stateCookieName, request));
|
|
180
236
|
headers.set("location", new URL(returnTo, requestUrl.origin).toString());
|
|
181
237
|
return new Response(null, {
|
|
182
238
|
status: 302,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farm.js/workos",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.98",
|
|
4
4
|
"description": "WorkOS integration for Farm.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@workos-inc/node": "^8.9.0",
|
|
33
|
-
"@farm.js/
|
|
34
|
-
"@farm.js/
|
|
33
|
+
"@farm.js/core": "0.1.0-beta.98",
|
|
34
|
+
"@farm.js/integration-utils": "0.1.0-beta.98"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|