@farm.js/workos 0.1.0-beta.10 → 0.1.0-beta.101
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 +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +118 -32
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { WorkOS } from "@workos-inc/node";
|
|
1
2
|
import { type FarmIntegrationLogger } from "@farm.js/core";
|
|
2
3
|
import type { WorkOSRedirectResult } from "./client.js";
|
|
3
4
|
export interface WorkOSIntegrationInput {
|
|
5
|
+
/** Existing WorkOS SDK instance. When provided, Farm does not construct its own client. */
|
|
6
|
+
instance?: WorkOSIntegrationInstance;
|
|
4
7
|
clientId?: string;
|
|
5
8
|
apiKey?: string;
|
|
6
9
|
cookiePassword?: string;
|
|
@@ -11,11 +14,18 @@ export interface WorkOSIntegrationInput {
|
|
|
11
14
|
logoutPath?: string;
|
|
12
15
|
sessionPath?: string;
|
|
13
16
|
protectedRoutes?: string | string[];
|
|
17
|
+
/**
|
|
18
|
+
* Additional origins allowed to post the sign-out route, using the same
|
|
19
|
+
* pattern syntax as `serverActions.allowedOrigins`. The app's own origin is
|
|
20
|
+
* always trusted.
|
|
21
|
+
*/
|
|
22
|
+
allowedOrigins?: string[];
|
|
14
23
|
log?: FarmIntegrationLogger;
|
|
15
24
|
}
|
|
25
|
+
export type WorkOSIntegrationInstance = WorkOS;
|
|
16
26
|
interface ResolvedWorkOSConfig {
|
|
17
27
|
clientId: string;
|
|
18
|
-
apiKey
|
|
28
|
+
apiKey?: string;
|
|
19
29
|
cookiePassword: string;
|
|
20
30
|
}
|
|
21
31
|
export declare function workos(input?: WorkOSIntegrationInput): import("@farm.js/core").DefinedIntegration<{
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAuC,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;
|
|
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;AAgBhG,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;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAoOG,MAAM;;;;yBAyE7B,OAAO;;GAe3C"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
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";
|
|
4
|
+
import { describeIntegrationOriginRejection, resolveIntegrationAllowedOrigins, validateIntegrationRequestOrigin, } from "@farm.js/core/integrations";
|
|
3
5
|
import { clearRequestCookie, createPathInferredClientApi, createDocumentNavigationMatchers, createRequestCookie, getCookieValue, getReturnTo, integrationConfig, normalizeMatchers, } from "@farm.js/integration-utils";
|
|
4
6
|
import { workosClient } from "./client.js";
|
|
5
7
|
const DEV_COOKIE_PASSWORD = "farmjs-workos-cookie-password-development-2026";
|
|
8
|
+
// The built server re-evaluates farm.config.ts at runtime to instantiate
|
|
9
|
+
// integrations, and Farm does not force NODE_ENV=production into that process.
|
|
10
|
+
// Treating an absent NODE_ENV as "development" would silently seal production
|
|
11
|
+
// sessions with the public DEV_COOKIE_PASSWORD, so only an explicit dev/test
|
|
12
|
+
// value may use it. `farm dev` runs through Vite, which sets
|
|
13
|
+
// NODE_ENV="development".
|
|
14
|
+
function isExplicitDevelopmentEnv() {
|
|
15
|
+
return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
16
|
+
}
|
|
6
17
|
function createWorkOSApi(input) {
|
|
7
18
|
return createPathInferredClientApi({
|
|
8
19
|
path: input.loginPath,
|
|
@@ -19,21 +30,23 @@ function createWorkOSApi(input) {
|
|
|
19
30
|
});
|
|
20
31
|
}
|
|
21
32
|
function resolveEnv(input) {
|
|
22
|
-
const clientId = input.clientId ?? process.env.WORKOS_CLIENT_ID ?? "";
|
|
33
|
+
const clientId = input.instance?.clientId ?? input.clientId ?? process.env.WORKOS_CLIENT_ID ?? "";
|
|
23
34
|
const apiKey = input.apiKey ?? process.env.WORKOS_API_KEY ?? "";
|
|
24
35
|
const cookiePassword = input.cookiePassword ??
|
|
25
36
|
process.env.WORKOS_COOKIE_PASSWORD ??
|
|
26
37
|
process.env.FARM_WORKOS_COOKIE_PASSWORD ??
|
|
27
|
-
(
|
|
28
|
-
if (!clientId || !apiKey) {
|
|
29
|
-
throw new Error("WorkOS integration requires
|
|
38
|
+
(isExplicitDevelopmentEnv() ? DEV_COOKIE_PASSWORD : "");
|
|
39
|
+
if (!clientId || (!input.instance && !apiKey)) {
|
|
40
|
+
throw new Error("WorkOS integration requires a client ID and either a WorkOS instance or WORKOS_API_KEY.");
|
|
30
41
|
}
|
|
31
42
|
if (!cookiePassword) {
|
|
32
|
-
throw new Error("WorkOS integration requires WORKOS_COOKIE_PASSWORD
|
|
43
|
+
throw new Error("WorkOS integration requires WORKOS_COOKIE_PASSWORD. A development-only fallback is used " +
|
|
44
|
+
'only when NODE_ENV is "development" or "test"; set WORKOS_COOKIE_PASSWORD to a random ' +
|
|
45
|
+
"32-byte value in every other environment.");
|
|
33
46
|
}
|
|
34
47
|
return {
|
|
35
48
|
clientId,
|
|
36
|
-
apiKey,
|
|
49
|
+
apiKey: apiKey || undefined,
|
|
37
50
|
cookiePassword,
|
|
38
51
|
};
|
|
39
52
|
}
|
|
@@ -64,23 +77,89 @@ export function workos(input = {}) {
|
|
|
64
77
|
const callbackPath = input.callbackPath ?? "/callback";
|
|
65
78
|
const logoutPath = input.logoutPath ?? "/logout";
|
|
66
79
|
const sessionPath = input.sessionPath ?? "/auth/session";
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
const allowedOrigins = resolveIntegrationAllowedOrigins(input.allowedOrigins, "workos.allowedOrigins");
|
|
81
|
+
const workos = input.instance ??
|
|
82
|
+
new WorkOS({
|
|
83
|
+
apiKey,
|
|
84
|
+
clientId,
|
|
85
|
+
});
|
|
86
|
+
// The OAuth `state` must be unguessable and bound to the browser that started
|
|
87
|
+
// the flow. We keep a random nonce in an HMAC-signed, http-only cookie and
|
|
88
|
+
// require the callback's state to match it, so an attacker cannot feed a
|
|
89
|
+
// victim a code/state pair from their own login (login CSRF / code injection).
|
|
90
|
+
const stateCookieName = "farm_workos_state";
|
|
91
|
+
function signState(payload) {
|
|
92
|
+
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
93
|
+
const signature = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
|
|
94
|
+
return `${encoded}.${signature}`;
|
|
95
|
+
}
|
|
96
|
+
function unsignState(signed) {
|
|
97
|
+
if (!signed)
|
|
98
|
+
return null;
|
|
99
|
+
const separator = signed.lastIndexOf(".");
|
|
100
|
+
if (separator <= 0)
|
|
101
|
+
return null;
|
|
102
|
+
const encoded = signed.slice(0, separator);
|
|
103
|
+
const signature = signed.slice(separator + 1);
|
|
104
|
+
const expected = createHmac("sha256", cookiePassword).update(encoded).digest("hex");
|
|
105
|
+
if (signature.length !== expected.length)
|
|
106
|
+
return null;
|
|
107
|
+
if (!timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
112
|
+
if (!parsed || typeof parsed !== "object")
|
|
113
|
+
return null;
|
|
114
|
+
const { state, returnTo } = parsed;
|
|
115
|
+
if (typeof state !== "string" || typeof returnTo !== "string")
|
|
116
|
+
return null;
|
|
117
|
+
return { state, returnTo };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Reject a sign-out request that did not come from this app. Returns the
|
|
125
|
+
* response to send, or null when the request may proceed.
|
|
126
|
+
*/
|
|
127
|
+
function rejectForeignOrigin(request) {
|
|
128
|
+
const result = validateIntegrationRequestOrigin(request, {
|
|
129
|
+
allowedOrigins,
|
|
130
|
+
requireOriginMetadata: request.method === "POST",
|
|
131
|
+
});
|
|
132
|
+
if (result.ok) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const message = describeIntegrationOriginRejection(result.reason);
|
|
136
|
+
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
137
|
+
return Response.json({ error: message }, { status: 403 });
|
|
138
|
+
}
|
|
139
|
+
return new Response(message, {
|
|
140
|
+
status: 403,
|
|
141
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
71
144
|
async function redirectToAuth(request, screenHint) {
|
|
72
145
|
const requestUrl = new URL(request.url);
|
|
73
146
|
const returnTo = getReturnTo(requestUrl.searchParams.get("returnTo"), "/dashboard");
|
|
74
147
|
const callbackUrl = new URL(callbackPath, requestUrl.origin);
|
|
148
|
+
const state = randomBytes(16).toString("hex");
|
|
75
149
|
const authorizationUrl = workos.userManagement.getAuthorizationUrl({
|
|
76
150
|
provider: "authkit",
|
|
77
151
|
clientId,
|
|
78
152
|
redirectUri: callbackUrl.toString(),
|
|
79
153
|
screenHint,
|
|
80
|
-
state
|
|
154
|
+
state,
|
|
81
155
|
});
|
|
156
|
+
const headers = new Headers();
|
|
157
|
+
headers.append("set-cookie", createRequestCookie(stateCookieName, signState({ state, returnTo }), request, {
|
|
158
|
+
maxAge: 600,
|
|
159
|
+
}));
|
|
82
160
|
return {
|
|
83
|
-
redirectTo: authorizationUrl,
|
|
161
|
+
result: { redirectTo: authorizationUrl },
|
|
162
|
+
headers,
|
|
84
163
|
};
|
|
85
164
|
}
|
|
86
165
|
return defineIntegration({
|
|
@@ -103,7 +182,9 @@ export function workos(input = {}) {
|
|
|
103
182
|
apiKey,
|
|
104
183
|
cookiePassword,
|
|
105
184
|
},
|
|
106
|
-
required:
|
|
185
|
+
required: input.instance
|
|
186
|
+
? ["clientId", "cookiePassword"]
|
|
187
|
+
: ["clientId", "apiKey", "cookiePassword"],
|
|
107
188
|
}),
|
|
108
189
|
api: createWorkOSApi({
|
|
109
190
|
loginPath,
|
|
@@ -121,21 +202,23 @@ export function workos(input = {}) {
|
|
|
121
202
|
integrationRoute.get(loginPath, {
|
|
122
203
|
responseFormat: "json",
|
|
123
204
|
async handler(request) {
|
|
124
|
-
const result = await redirectToAuth(request, "sign-in");
|
|
205
|
+
const { result, headers } = await redirectToAuth(request, "sign-in");
|
|
125
206
|
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
126
|
-
return Response.json(result);
|
|
207
|
+
return Response.json(result, { headers });
|
|
127
208
|
}
|
|
128
|
-
|
|
209
|
+
headers.set("location", result.redirectTo);
|
|
210
|
+
return new Response(null, { status: 302, headers });
|
|
129
211
|
},
|
|
130
212
|
}),
|
|
131
213
|
integrationRoute.get(signUpPath, {
|
|
132
214
|
responseFormat: "json",
|
|
133
215
|
async handler(request) {
|
|
134
|
-
const result = await redirectToAuth(request, "sign-up");
|
|
216
|
+
const { result, headers } = await redirectToAuth(request, "sign-up");
|
|
135
217
|
if (request.headers.get("x-farm-integration-client") === "1") {
|
|
136
|
-
return Response.json(result);
|
|
218
|
+
return Response.json(result, { headers });
|
|
137
219
|
}
|
|
138
|
-
|
|
220
|
+
headers.set("location", result.redirectTo);
|
|
221
|
+
return new Response(null, { status: 302, headers });
|
|
139
222
|
},
|
|
140
223
|
}),
|
|
141
224
|
integrationRoute.get(callbackPath, {
|
|
@@ -150,6 +233,13 @@ export function workos(input = {}) {
|
|
|
150
233
|
if (!code) {
|
|
151
234
|
return new Response("Missing WorkOS authorization code.", { status: 400 });
|
|
152
235
|
}
|
|
236
|
+
// Bind the callback to the browser that started the flow. Verify before
|
|
237
|
+
// the code is exchanged so an injected code is never redeemed.
|
|
238
|
+
const statePayload = unsignState(getCookieValue(request.headers, stateCookieName));
|
|
239
|
+
const state = requestUrl.searchParams.get("state");
|
|
240
|
+
if (!statePayload || !state || statePayload.state !== state) {
|
|
241
|
+
return new Response("Invalid WorkOS authentication state.", { status: 400 });
|
|
242
|
+
}
|
|
153
243
|
const authentication = await workos.userManagement.authenticateWithCode({
|
|
154
244
|
clientId,
|
|
155
245
|
code,
|
|
@@ -161,19 +251,11 @@ export function workos(input = {}) {
|
|
|
161
251
|
if (!authentication.sealedSession) {
|
|
162
252
|
return new Response("WorkOS did not return a sealed session.", { status: 500 });
|
|
163
253
|
}
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
if (rawState) {
|
|
167
|
-
try {
|
|
168
|
-
const parsed = JSON.parse(rawState);
|
|
169
|
-
returnTo = getReturnTo(parsed.returnTo ?? null, "/dashboard");
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
returnTo = "/dashboard";
|
|
173
|
-
}
|
|
174
|
-
}
|
|
254
|
+
// Taken from the signed cookie, never from the untrusted query string.
|
|
255
|
+
const returnTo = getReturnTo(statePayload.returnTo, "/dashboard");
|
|
175
256
|
const headers = new Headers();
|
|
176
|
-
headers.
|
|
257
|
+
headers.append("set-cookie", createRequestCookie(cookieName, authentication.sealedSession, request));
|
|
258
|
+
headers.append("set-cookie", clearRequestCookie(stateCookieName, request));
|
|
177
259
|
headers.set("location", new URL(returnTo, requestUrl.origin).toString());
|
|
178
260
|
return new Response(null, {
|
|
179
261
|
status: 302,
|
|
@@ -184,6 +266,10 @@ export function workos(input = {}) {
|
|
|
184
266
|
integrationRoute.post(logoutPath, {
|
|
185
267
|
responseFormat: "json",
|
|
186
268
|
async handler(request) {
|
|
269
|
+
const rejected = rejectForeignOrigin(request);
|
|
270
|
+
if (rejected) {
|
|
271
|
+
return rejected;
|
|
272
|
+
}
|
|
187
273
|
const requestUrl = new URL(request.url);
|
|
188
274
|
const sessionState = await getSession(workos, request, cookieName, cookiePassword);
|
|
189
275
|
const headers = new Headers();
|
|
@@ -243,7 +329,7 @@ export function workos(input = {}) {
|
|
|
243
329
|
const requestUrl = new URL(request.url);
|
|
244
330
|
const loginUrl = new URL(loginPath, request.url);
|
|
245
331
|
loginUrl.searchParams.set("returnTo", `${requestUrl.pathname}${requestUrl.search}`);
|
|
246
|
-
return Response.redirect(loginUrl,
|
|
332
|
+
return Response.redirect(loginUrl, 303); // 303 (See Other) mandates a GET on the GET-only login route regardless of the original method (RFC 7231 §6.4.4)
|
|
247
333
|
},
|
|
248
334
|
},
|
|
249
335
|
]
|
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.101",
|
|
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/integration-utils": "0.1.0-beta.101",
|
|
34
|
+
"@farm.js/core": "0.1.0-beta.101"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|