@astralarium/repo-auth 0.1.0 → 0.2.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/README.md +49 -0
- package/dist/client.d.ts +32 -16
- package/dist/client.js +42 -33
- package/dist/dev-sign-in/client.d.ts +21 -0
- package/dist/dev-sign-in/client.js +17 -0
- package/dist/dev-sign-in/endpoint.d.ts +2 -0
- package/dist/dev-sign-in/endpoint.js +2 -0
- package/dist/dev-sign-in/index.d.ts +4 -0
- package/dist/dev-sign-in/index.js +4 -0
- package/dist/dev-sign-in/provider-user.d.ts +30 -0
- package/dist/dev-sign-in/provider-user.js +55 -0
- package/dist/dev-sign-in/sign-in.d.ts +62 -0
- package/dist/dev-sign-in/sign-in.js +116 -0
- package/dist/index.server.d.ts +36 -11
- package/dist/index.server.js +4 -9
- package/dist/request.d.ts +2 -2
- package/dist/request.js +5 -3
- package/dist/start/index.d.ts +6 -0
- package/dist/start/index.js +6 -0
- package/dist/start/index.server.d.ts +28 -0
- package/dist/start/index.server.js +29 -0
- package/dist/start/middleware.d.ts +33 -0
- package/dist/start/middleware.js +70 -0
- package/dist/start/return-path.d.ts +16 -0
- package/dist/start/return-path.js +32 -0
- package/dist/start/viewer.d.ts +8 -0
- package/dist/start/viewer.js +15 -0
- package/dist/testkit/index.d.ts +18 -8
- package/dist/testkit/index.js +74 -43
- package/dist/testkit/suites.d.ts +52 -12
- package/dist/testkit/suites.js +114 -24
- package/package.json +19 -7
package/dist/testkit/suites.js
CHANGED
|
@@ -1,47 +1,100 @@
|
|
|
1
1
|
/** Test suites of an app's served auth: its dev build over the app's
|
|
2
|
-
* Postgres, and its production build. */
|
|
2
|
+
* Postgres, and its production build; dev sign-in through the app. */
|
|
3
3
|
import { expectViewerIndependent } from "@astralarium/repo-config-vitest/pages";
|
|
4
4
|
import { withSql } from "@astralarium/repo-postgres/sql";
|
|
5
5
|
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { DEV_SIGN_IN_ENDPOINT } from "../dev-sign-in/endpoint.js";
|
|
6
8
|
import { shellAuth } from "../index.server.js";
|
|
7
9
|
import { AUTH_BASE_PATH } from "../request.js";
|
|
8
10
|
import { authorizePaths, cookieHeader, cookiePairs, expectAnonymousSession, expectAuthRateLimited, idTokenAuthorization, socialSignIn, unservedAuthPaths, verifyAuthSchema, withBlockedSessionRead } from "./index.js";
|
|
9
11
|
import { jsonPost } from "./json-post.js";
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
export function
|
|
12
|
+
/** Dev sign-in through the app at `origin` as `accountId`
|
|
13
|
+
* (`ShellAuthApp.devSignIn`): its answer. */
|
|
14
|
+
export function devSignIn(request, origin, accountId) {
|
|
15
|
+
return request(`${AUTH_BASE_PATH}${DEV_SIGN_IN_ENDPOINT}`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
origin,
|
|
19
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
20
|
+
},
|
|
21
|
+
body: new URLSearchParams({
|
|
22
|
+
accountId
|
|
23
|
+
}).toString()
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Signs in through the app's dev sign-in at `origin` as `accountId`;
|
|
27
|
+
* rejects naming any answer other than 200. */
|
|
28
|
+
export async function devSignInAs(request, origin, accountId) {
|
|
29
|
+
const response = await devSignIn(request, origin, accountId);
|
|
30
|
+
const body = await response.text();
|
|
31
|
+
if (response.status !== 200) throw new Error(`dev sign-in as ${accountId}: HTTP ${response.status} ${body}`);
|
|
32
|
+
const {
|
|
33
|
+
user
|
|
34
|
+
} = z.object({
|
|
35
|
+
user: z.object({
|
|
36
|
+
id: z.string()
|
|
37
|
+
})
|
|
38
|
+
}).parse(JSON.parse(body));
|
|
39
|
+
const setCookies = response.headers.getSetCookie();
|
|
40
|
+
return {
|
|
41
|
+
userId: user.id,
|
|
42
|
+
cookie: cookieHeader(cookiePairs(setCookies)),
|
|
43
|
+
setCookies
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The session and user ids `cookie` carries, read past the cookie
|
|
47
|
+
* cache. */
|
|
48
|
+
export async function sessionOf(request, cookie) {
|
|
49
|
+
const response = await request(`${AUTH_BASE_PATH}/get-session?disableCookieCache=true`, {
|
|
50
|
+
headers: {
|
|
51
|
+
cookie
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return z.object({
|
|
55
|
+
session: z.object({
|
|
56
|
+
id: z.string()
|
|
57
|
+
}),
|
|
58
|
+
user: z.object({
|
|
59
|
+
id: z.string()
|
|
60
|
+
})
|
|
61
|
+
}).parse(JSON.parse(await response.text()));
|
|
62
|
+
}
|
|
63
|
+
/** A session signed in through the app's dev sign-in at `origin` as a
|
|
64
|
+
* fresh `devAccountId()` (apps with `devSignIn`); its new user is deleted
|
|
65
|
+
* from `db` at `end`. */
|
|
66
|
+
export function devSignInSession(request, {
|
|
13
67
|
origin,
|
|
14
|
-
db
|
|
68
|
+
db,
|
|
69
|
+
devAccountId
|
|
15
70
|
}) {
|
|
16
|
-
|
|
71
|
+
let userId;
|
|
17
72
|
return {
|
|
18
73
|
async start() {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
password: "test-password-123"
|
|
23
|
-
}));
|
|
24
|
-
expect(response.status).toBe(200);
|
|
25
|
-
await response.body?.cancel();
|
|
26
|
-
const cookie = cookieHeader(cookiePairs(response.headers.getSetCookie()));
|
|
27
|
-
expect(cookie).toContain("session_token=");
|
|
28
|
-
return cookie;
|
|
74
|
+
const signedIn = await devSignInAs(request, origin, devAccountId());
|
|
75
|
+
userId = signedIn.userId;
|
|
76
|
+
return signedIn.cookie;
|
|
29
77
|
},
|
|
30
78
|
async end() {
|
|
79
|
+
if (!userId) return;
|
|
80
|
+
const id = userId;
|
|
31
81
|
await withSql(db, async sql => {
|
|
32
|
-
await sql`DELETE FROM auth.users WHERE
|
|
82
|
+
await sql`DELETE FROM auth.users WHERE id = ${id}`;
|
|
33
83
|
});
|
|
34
84
|
}
|
|
35
85
|
};
|
|
36
86
|
}
|
|
37
87
|
/**
|
|
38
88
|
* Tests of `request`'s dev app serving auth over its Postgres: the database
|
|
39
|
-
* role, public pages independent of the viewer,
|
|
40
|
-
* each local origin, and the rate limit, last: it exhausts
|
|
41
|
-
* budget for the window.
|
|
89
|
+
* role, public pages independent of the viewer, dev sign-in, provider
|
|
90
|
+
* callbacks on each local origin, and the rate limit, last: it exhausts
|
|
91
|
+
* the client's budget for the window. Users dev sign-in creates are
|
|
92
|
+
* deleted from `db` after.
|
|
42
93
|
*/
|
|
43
94
|
export function testDevAuth(request, {
|
|
44
95
|
session,
|
|
96
|
+
origin,
|
|
97
|
+
devAccountId,
|
|
45
98
|
db,
|
|
46
99
|
role,
|
|
47
100
|
publicPages,
|
|
@@ -53,10 +106,24 @@ export function testDevAuth(request, {
|
|
|
53
106
|
concurrent: false
|
|
54
107
|
}, () => {
|
|
55
108
|
let cookie = "";
|
|
109
|
+
/** Users dev sign-in created. */
|
|
110
|
+
const devUsers = new Set();
|
|
56
111
|
beforeAll(async () => {
|
|
57
112
|
cookie = await session.start();
|
|
58
113
|
});
|
|
59
|
-
afterAll(() =>
|
|
114
|
+
afterAll(async () => {
|
|
115
|
+
await session.end();
|
|
116
|
+
await withSql(db, async sql => {
|
|
117
|
+
await sql`DELETE FROM auth.users WHERE id = ANY(${[...devUsers]}::uuid[])`;
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
/** Signs in as `accountId`: the session's user id and the response's
|
|
121
|
+
* `Set-Cookie` values. */
|
|
122
|
+
async function signInAs(accountId) {
|
|
123
|
+
const signedIn = await devSignInAs(request, origin, accountId);
|
|
124
|
+
devUsers.add(signedIn.userId);
|
|
125
|
+
return signedIn;
|
|
126
|
+
}
|
|
60
127
|
test(`the app queries Postgres as ${role}, a non-superuser`, async () => {
|
|
61
128
|
const blocked = await withBlockedSessionRead(db, () => request(`${AUTH_BASE_PATH}/get-session?disableCookieCache=true`, {
|
|
62
129
|
headers: {
|
|
@@ -70,6 +137,24 @@ export function testDevAuth(request, {
|
|
|
70
137
|
expect((await blocked.response).status).toBe(200);
|
|
71
138
|
}, 15_000);
|
|
72
139
|
test.each(publicPages)("the public page %s serves a signed-in visitor the anonymous visitor's document", url => expectViewerIndependent(request, url, cookie));
|
|
140
|
+
test(`dev sign-in signs in as a new ${provider} account's user with better-auth's session cookies and a signed marker naming the session`, async () => {
|
|
141
|
+
const signedIn = await signInAs(devAccountId());
|
|
142
|
+
const fresh = await sessionOf(request, signedIn.cookie);
|
|
143
|
+
expect(fresh.user.id).toBe(signedIn.userId);
|
|
144
|
+
expect(signedIn.setCookies).toContainEqual(expect.stringMatching(new RegExp(`\\.dev_sign_in=${fresh.session.id}\\.[^;]+;.*HttpOnly`, "i")));
|
|
145
|
+
});
|
|
146
|
+
test(`dev sign-in as an existing ${provider} account signs in as its user`, async () => {
|
|
147
|
+
const accountId = devAccountId();
|
|
148
|
+
const first = await signInAs(accountId);
|
|
149
|
+
const second = await signInAs(accountId);
|
|
150
|
+
expect(second.userId).toBe(first.userId);
|
|
151
|
+
expect((await sessionOf(request, second.cookie)).user.id).toBe(first.userId);
|
|
152
|
+
});
|
|
153
|
+
test("dev sign-in refuses a cross-origin request", async () => {
|
|
154
|
+
const response = await devSignIn(request, "https://elsewhere.invalid", devAccountId());
|
|
155
|
+
expect(response.status).toBe(403);
|
|
156
|
+
await response.body?.cancel();
|
|
157
|
+
});
|
|
73
158
|
test.each(localOrigins.flatMap(({
|
|
74
159
|
origin,
|
|
75
160
|
requesters
|
|
@@ -94,8 +179,8 @@ export function testDevAuth(request, {
|
|
|
94
179
|
}
|
|
95
180
|
/**
|
|
96
181
|
* Tests of `request`'s production app serving auth, against `app` built for
|
|
97
|
-
* production (`
|
|
98
|
-
* endpoints, ID-token refusal, the anonymous session, a failed provider
|
|
182
|
+
* production (`devSignIn` off): its schema, 404s outside the served
|
|
183
|
+
* endpoints and at dev sign-in, ID-token refusal, the anonymous session, a failed provider
|
|
99
184
|
* callback landing on the error page, and each provider's sign-in with its
|
|
100
185
|
* runtime client and callback.
|
|
101
186
|
*/
|
|
@@ -109,7 +194,7 @@ export function testProductionAuth(request, {
|
|
|
109
194
|
}) {
|
|
110
195
|
const production = {
|
|
111
196
|
...app,
|
|
112
|
-
|
|
197
|
+
devSignIn: undefined
|
|
113
198
|
};
|
|
114
199
|
const auth = shellAuth(() => ({
|
|
115
200
|
...production,
|
|
@@ -131,6 +216,11 @@ export function testProductionAuth(request, {
|
|
|
131
216
|
expect(response.status).toBe(404);
|
|
132
217
|
expect(await response.text()).toBe("Not Found");
|
|
133
218
|
});
|
|
219
|
+
test("the production app returns not found for dev sign-in", async () => {
|
|
220
|
+
const response = await devSignIn(request, origin, "1");
|
|
221
|
+
expect(response.status).toBe(404);
|
|
222
|
+
await response.body?.cancel();
|
|
223
|
+
});
|
|
134
224
|
test.each(authorizePaths(production))("the production app refuses ID-token authorization at %s", async path => {
|
|
135
225
|
const response = await request(path, idTokenAuthorization({
|
|
136
226
|
origin,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astralarium/repo-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://github.com/astralarium/repo/tree/main/packages/repo-auth#readme",
|
|
6
6
|
"repository": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"directory": "packages/repo-auth"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
12
13
|
"files": [
|
|
13
14
|
"dist",
|
|
14
15
|
"sql"
|
|
@@ -16,9 +17,13 @@
|
|
|
16
17
|
"exports": {
|
|
17
18
|
".": "./dist/index.server.js",
|
|
18
19
|
"./client": "./dist/client.js",
|
|
20
|
+
"./dev-sign-in": "./dist/dev-sign-in/index.js",
|
|
21
|
+
"./dev-sign-in/client": "./dist/dev-sign-in/client.js",
|
|
19
22
|
"./providers": "./dist/providers/index.js",
|
|
20
23
|
"./request": "./dist/request.js",
|
|
21
24
|
"./schema": "./dist/schema.js",
|
|
25
|
+
"./start": "./dist/start/index.js",
|
|
26
|
+
"./start/server": "./dist/start/index.server.js",
|
|
22
27
|
"./testkit": "./dist/testkit/index.js",
|
|
23
28
|
"./testkit/suites": "./dist/testkit/suites.js"
|
|
24
29
|
},
|
|
@@ -29,12 +34,14 @@
|
|
|
29
34
|
"kysely": "^0.29.5",
|
|
30
35
|
"pg": "^8.20.0",
|
|
31
36
|
"zod": "^4.6.7",
|
|
32
|
-
"@astralarium/repo-env": "^0.1.0",
|
|
33
37
|
"@astralarium/repo-package-files": "^0.1.0",
|
|
34
|
-
"@astralarium/repo-
|
|
35
|
-
"@astralarium/repo-
|
|
38
|
+
"@astralarium/repo-telemetry": "^0.2.0",
|
|
39
|
+
"@astralarium/repo-postgres": "^0.2.0",
|
|
40
|
+
"@astralarium/repo-env": "^0.2.0"
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
43
|
+
"@tanstack/query-core": "^5.107.1",
|
|
44
|
+
"@tanstack/react-start": "^1.169.4",
|
|
38
45
|
"@types/node": "^24.2.0",
|
|
39
46
|
"@types/pg": "^8.20.0",
|
|
40
47
|
"@types/react": "19.2.14",
|
|
@@ -43,22 +50,27 @@
|
|
|
43
50
|
"postgres": "^3.4.9",
|
|
44
51
|
"react": "19.2.5",
|
|
45
52
|
"typescript": "^6.0.3",
|
|
53
|
+
"vite": "^8.3.4",
|
|
46
54
|
"vitest": "^5.0.3",
|
|
47
|
-
"@astralarium/repo-config-eslint": "^0.1.0",
|
|
48
55
|
"@astralarium/repo-config-typescript": "^0.1.0",
|
|
49
|
-
"@astralarium/repo-config-vitest": "^0.
|
|
56
|
+
"@astralarium/repo-config-vitest": "^0.2.0",
|
|
57
|
+
"@astralarium/repo-config-eslint": "^0.3.0",
|
|
50
58
|
"@astralarium/repo-package-build": "^0.1.0"
|
|
51
59
|
},
|
|
52
60
|
"peerDependencies": {
|
|
61
|
+
"@tanstack/react-start": "^1.169.4",
|
|
53
62
|
"better-auth": "^1.7.7",
|
|
54
63
|
"react": "^19.2.0",
|
|
55
64
|
"vitest": "^5.0.3",
|
|
56
|
-
"@astralarium/repo-config-vitest": "^0.
|
|
65
|
+
"@astralarium/repo-config-vitest": "^0.2.0"
|
|
57
66
|
},
|
|
58
67
|
"peerDependenciesMeta": {
|
|
59
68
|
"@astralarium/repo-config-vitest": {
|
|
60
69
|
"optional": true
|
|
61
70
|
},
|
|
71
|
+
"@tanstack/react-start": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
62
74
|
"react": {
|
|
63
75
|
"optional": true
|
|
64
76
|
},
|