@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.
@@ -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
- /** A throwaway email user signed up through the app at `origin` (apps
11
- * with `devEmailSignIn`), deleted from `db` at `end`. */
12
- export function emailSession(request, {
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
- const email = `session-${crypto.randomUUID()}@example.test`;
71
+ let userId;
17
72
  return {
18
73
  async start() {
19
- const response = await request(`${AUTH_BASE_PATH}/sign-up/email`, jsonPost(origin, {
20
- name: "Test Session",
21
- email,
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 email = ${email}`;
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, provider callbacks on
40
- * each local origin, and the rate limit, last: it exhausts the client's
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(() => session.end());
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 (`devEmailSignIn` off): its schema, 404s outside the served
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
- devEmailSignIn: false
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.1.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-postgres": "^0.1.0",
35
- "@astralarium/repo-telemetry": "^0.1.0"
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.1.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.1.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
  },