@jxsuite/auth 0.1.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 +97 -0
- package/jx-extension.json +13 -0
- package/package.json +51 -0
- package/schemas/project.fragment.schema.json +76 -0
- package/src/Auth.class.json +135 -0
- package/src/AuthActions.class.json +51 -0
- package/src/Session.class.json +50 -0
- package/src/actions.ts +149 -0
- package/src/authorize.ts +92 -0
- package/src/client.ts +76 -0
- package/src/config.ts +163 -0
- package/src/server.ts +218 -0
- package/src/session.ts +118 -0
- package/src/worker.ts +180 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# `@jxsuite/auth`
|
|
2
|
+
|
|
3
|
+
> Better Auth sessions, sign-in flows, and table permissions for Jx sites.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@jxsuite/auth` is the Jx extension putting users behind the connector's data tables. It owns the
|
|
8
|
+
project.json `auth` section and mounts [Better Auth](https://better-auth.com) (pinned 1.6.23) at
|
|
9
|
+
`/_jx/auth` — **order 10**, ahead of the connector's `/_jx/data` mount (order 20) — publishing the
|
|
10
|
+
`ctx.auth = { getSession, authorize }` hooks on the shared server context
|
|
11
|
+
(specs/extensions.md §11). With this mount active, table permission rules beyond
|
|
12
|
+
`public`/`none` come alive:
|
|
13
|
+
|
|
14
|
+
| Rule | Grants |
|
|
15
|
+
| --------------- | ------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| `authenticated` | Any signed-in user. |
|
|
17
|
+
| `owner` | Inserts stamp the table's `ownerField` with the user id; reads/updates/deletes scope to that row. |
|
|
18
|
+
| `role:<r>` | Users whose `role` column equals `<r>`. |
|
|
19
|
+
|
|
20
|
+
Without the auth mount the data mount **fails closed**: only `public` rules pass.
|
|
21
|
+
|
|
22
|
+
Declaring an `ownerField` makes that column authoritative: **every** session-granted insert
|
|
23
|
+
(`authenticated`, `owner`, `role:<r>`) stamps it with the signed-in user's id, so clients can
|
|
24
|
+
never forge ownership.
|
|
25
|
+
|
|
26
|
+
The auth system tables (`user`, `session`, `account`, `verification`) live on an ordinary
|
|
27
|
+
connector connection — `auth.connection`, defaulting to the project's first-declared connection —
|
|
28
|
+
and are created by Better Auth's additive migrations: `jx db push` (or the Studio's push button)
|
|
29
|
+
plans them as `kind: "auth"` steps after the connector plan, and the dev server also syncs them on
|
|
30
|
+
first touch of `/_jx/auth`.
|
|
31
|
+
|
|
32
|
+
## Enable it
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"extensions": ["@jxsuite/parser", "@jxsuite/connector", "@jxsuite/auth"],
|
|
37
|
+
"connections": { "main": { "provider": "sqlite" } },
|
|
38
|
+
"auth": {
|
|
39
|
+
"connection": "main",
|
|
40
|
+
"methods": { "emailPassword": true },
|
|
41
|
+
"redirects": { "afterSignIn": "/", "afterSignOut": "/" }
|
|
42
|
+
},
|
|
43
|
+
"data": {
|
|
44
|
+
"comments": {
|
|
45
|
+
"connection": "main",
|
|
46
|
+
"permissions": { "read": "public", "insert": "authenticated", "update": "owner" },
|
|
47
|
+
"ownerField": "author_id",
|
|
48
|
+
"schema": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": { "message": { "type": "string" }, "author_id": { "type": "string" } },
|
|
51
|
+
"required": ["message"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Secrets never enter project.json (specs/extensions.md §13): set the signing secret under the env
|
|
59
|
+
var named by `auth.secretEnv` (default `BETTER_AUTH_SECRET`) in `.dev.vars` locally and via
|
|
60
|
+
`wrangler secret put` in production. Social providers name their credential env vars the same way
|
|
61
|
+
(`providers.github.clientIdEnv`, defaulting to `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`).
|
|
62
|
+
|
|
63
|
+
## Page UX
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"state": {
|
|
68
|
+
"session": { "$prototype": "Session" },
|
|
69
|
+
"auth": { "$prototype": "AuthActions" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **`Session`** resolves to `{ userId, role?, user }` or `null` and updates live via
|
|
75
|
+
`subscribe()`. Outside browsers it resolves to `null`, so statically generated pages always
|
|
76
|
+
render the signed-out state.
|
|
77
|
+
- **`AuthActions`** resolves to `{ signInEmail, signUpEmail, signInSocial, signOut }` — wire a
|
|
78
|
+
form's `onsubmit` to `{ "$ref": "auth.signInEmail" }` and the handler reads the form's
|
|
79
|
+
`email`/`password` fields, refreshes the session store, bumps the `_v` read-after-write version,
|
|
80
|
+
and applies `auth.redirects`.
|
|
81
|
+
|
|
82
|
+
## v1 cuts
|
|
83
|
+
|
|
84
|
+
- **No email transport**: no verification or password-reset emails ship in v1 — email/password
|
|
85
|
+
accounts are live immediately after sign-up.
|
|
86
|
+
- **Roles are edited via the data grid**: declare role names in `auth.roles` (this adds the
|
|
87
|
+
`role` column to the user table); assign them by editing user rows in the Studio's data grid.
|
|
88
|
+
Sign-up input can never set a role.
|
|
89
|
+
- **Public-insert abuse**: a table with `insert: "public"` accepts writes from anyone on the
|
|
90
|
+
internet. Prefer `authenticated` inserts, or accept the risk knowingly (rate limiting /
|
|
91
|
+
Turnstile are future work).
|
|
92
|
+
|
|
93
|
+
## Versioning
|
|
94
|
+
|
|
95
|
+
Published to npm as `@jxsuite/auth` — TypeScript source, like every `@jxsuite` package, following
|
|
96
|
+
the monorepo's release train. Depends on `@jxsuite/connector` (the `resolveDialect` seam and the
|
|
97
|
+
permission/hook types it publishes for dependents); core packages never depend on this extension.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jxsuite/auth",
|
|
3
|
+
"title": "Authentication",
|
|
4
|
+
"description": "Better Auth sessions, sign-in flows, and table permissions served over /_jx/auth",
|
|
5
|
+
"classes": {
|
|
6
|
+
"Auth": "./src/Auth.class.json",
|
|
7
|
+
"Session": "./src/Session.class.json",
|
|
8
|
+
"AuthActions": "./src/AuthActions.class.json"
|
|
9
|
+
},
|
|
10
|
+
"schemas": {
|
|
11
|
+
"project": "./schemas/project.fragment.schema.json"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jxsuite/auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Better Auth sessions, sign-in flows, and table permissions for Jx sites over /_jx/auth",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jxsuite/jx.git",
|
|
9
|
+
"directory": "extensions/auth"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"jx-extension.json",
|
|
14
|
+
"schemas/"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/server.ts",
|
|
19
|
+
"./authorize": "./src/authorize.ts",
|
|
20
|
+
"./config": "./src/config.ts",
|
|
21
|
+
"./server": "./src/server.ts",
|
|
22
|
+
"./worker": "./src/worker.ts",
|
|
23
|
+
"./client": "./src/client.ts",
|
|
24
|
+
"./session": "./src/session.ts",
|
|
25
|
+
"./actions": "./src/actions.ts",
|
|
26
|
+
"./jx-extension.json": "./jx-extension.json",
|
|
27
|
+
"./schemas/project.fragment.schema.json": "./schemas/project.fragment.schema.json",
|
|
28
|
+
"./Auth.class.json": "./src/Auth.class.json",
|
|
29
|
+
"./Session.class.json": "./src/Session.class.json",
|
|
30
|
+
"./AuthActions.class.json": "./src/AuthActions.class.json"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"provenance": false
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "bun test --isolate",
|
|
37
|
+
"test:coverage": "bun test --isolate --coverage",
|
|
38
|
+
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@jxsuite/connector": "workspace:^",
|
|
42
|
+
"@jxsuite/schema": "workspace:^",
|
|
43
|
+
"better-auth": "1.6.23",
|
|
44
|
+
"kysely": "^0.28.17"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@happy-dom/global-registrator": "^20.10.6",
|
|
48
|
+
"ajv": "^8.20.0"
|
|
49
|
+
},
|
|
50
|
+
"jx": "./jx-extension.json"
|
|
51
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://jxsuite.com/schema/ext/auth/project/v1",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"title": "Auth project fragment",
|
|
5
|
+
"description": "Project-schema fragment contributed by @jxsuite/auth: the `auth` section configuring Better Auth over a connector connection. Entries carry identifiers and env-var NAMES only — secret values live in .dev.vars locally and the platform secret store in production (specs/extensions.md §13).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"auth": {
|
|
9
|
+
"description": "Authentication configuration. Enables the /_jx/auth mount (Better Auth) and the permission rules beyond \"public\"/\"none\" on data tables.",
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"connection": {
|
|
13
|
+
"description": "Name of a `connections` entry holding the auth system tables (user, session, account, verification). Defaults to the project's first-declared connection.",
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"secretEnv": {
|
|
17
|
+
"description": "NAME of the env var holding the Better Auth signing secret. The value itself is a secret and never enters project.json.",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"default": "BETTER_AUTH_SECRET"
|
|
20
|
+
},
|
|
21
|
+
"methods": {
|
|
22
|
+
"description": "Enabled first-party sign-in methods.",
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"emailPassword": {
|
|
26
|
+
"description": "Email + password sign-up/sign-in (enabled by default).",
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"default": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"providers": {
|
|
33
|
+
"description": "Social sign-in providers keyed by Better Auth provider id (\"github\", \"google\", …). Values name the env vars carrying each provider's OAuth credentials; defaults are <ID>_CLIENT_ID / <ID>_CLIENT_SECRET.",
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"clientIdEnv": {
|
|
39
|
+
"description": "NAME of the env var holding the OAuth client id.",
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"clientSecretEnv": {
|
|
43
|
+
"description": "NAME of the env var holding the OAuth client secret.",
|
|
44
|
+
"type": "string"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"redirects": {
|
|
50
|
+
"description": "Site paths the auth actions navigate to after auth state changes.",
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"afterSignIn": {
|
|
54
|
+
"description": "Path to navigate to after a successful sign-in/sign-up.",
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
"afterSignOut": {
|
|
58
|
+
"description": "Path to navigate to after sign-out.",
|
|
59
|
+
"type": "string"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"roles": {
|
|
64
|
+
"description": "Role names usable in table permission rules (\"role:<name>\"). Declaring any role adds a `role` column to the user table; assignments are edited via the data grid.",
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "type": "string" }
|
|
67
|
+
},
|
|
68
|
+
"trustedOrigins": {
|
|
69
|
+
"description": "Extra origins allowed to call the auth routes (Better Auth trustedOrigins passthrough).",
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": { "type": "string" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jxsuite.com/schemas/auth/Auth.class.json",
|
|
4
|
+
"title": "Auth",
|
|
5
|
+
"description": "Owner of the project.json `auth` section and the /_jx/auth server mount: Better Auth over a connector connection, publishing the ctx.auth hooks the data mount authorizes against",
|
|
6
|
+
"$prototype": "Class",
|
|
7
|
+
"extends": "Object",
|
|
8
|
+
"$implementation": "./worker.js",
|
|
9
|
+
|
|
10
|
+
"project": {
|
|
11
|
+
"key": "auth",
|
|
12
|
+
"title": "Authentication",
|
|
13
|
+
"description": "Better Auth sessions and sign-in methods served at /_jx/auth"
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"server": {
|
|
17
|
+
"basePath": "/_jx/auth",
|
|
18
|
+
"order": 10,
|
|
19
|
+
"module": "@jxsuite/auth/worker"
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
"$studio": {
|
|
23
|
+
"settings": {
|
|
24
|
+
"icon": "sp-icon-lock-closed",
|
|
25
|
+
"label": "Authentication",
|
|
26
|
+
"order": 58,
|
|
27
|
+
"layout": "form",
|
|
28
|
+
"entry": {
|
|
29
|
+
"ui": {
|
|
30
|
+
"connection": { "enum": { "$ref": "#/$context/connections" } },
|
|
31
|
+
"secretEnv": { "control": "secret" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"$defs": {
|
|
38
|
+
"returnTypes": {
|
|
39
|
+
"SectionData": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"description": "The auth section value (identifiers and env-var names only), stored as _project.auth"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
"methods": {
|
|
46
|
+
"projectData": {
|
|
47
|
+
"role": "projectData",
|
|
48
|
+
"$prototype": "Function",
|
|
49
|
+
"access": "public",
|
|
50
|
+
"scope": "static",
|
|
51
|
+
"identifier": "projectData",
|
|
52
|
+
"timing": ["compiler", "server"],
|
|
53
|
+
"parameters": [
|
|
54
|
+
{
|
|
55
|
+
"identifier": "sectionValue",
|
|
56
|
+
"type": { "type": "object" },
|
|
57
|
+
"description": "The project.json `auth` section value"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"identifier": "ctx",
|
|
61
|
+
"type": { "type": "object" },
|
|
62
|
+
"description": "Host context: { projectConfig, root, registry, io }"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"returnType": { "$ref": "#/$defs/returnTypes/SectionData" },
|
|
66
|
+
"description": "Expose the auth configuration under _project.auth (redirects for the auth actions; never secrets)"
|
|
67
|
+
},
|
|
68
|
+
"mount": {
|
|
69
|
+
"role": "mount",
|
|
70
|
+
"$prototype": "Function",
|
|
71
|
+
"access": "public",
|
|
72
|
+
"scope": "static",
|
|
73
|
+
"identifier": "mount",
|
|
74
|
+
"timing": ["server"],
|
|
75
|
+
"parameters": [
|
|
76
|
+
{
|
|
77
|
+
"identifier": "options",
|
|
78
|
+
"type": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"basePath": { "type": "string" },
|
|
82
|
+
"sections": { "type": "object" },
|
|
83
|
+
"connectors": { "type": "object" },
|
|
84
|
+
"autoSync": { "type": "boolean" }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"description": "Inlined mount options: project sections, provider constructors, sync mode"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"identifier": "ctx",
|
|
91
|
+
"type": { "type": "object" },
|
|
92
|
+
"description": "Shared JxServerContext — this mount (order 10) publishes ctx.auth for later mounts"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"returnType": { "type": "object" },
|
|
96
|
+
"description": "Fetch-style handler (request, env) → Response serving the Better Auth routes under /_jx/auth; sets ctx.auth = { getSession, authorize }"
|
|
97
|
+
},
|
|
98
|
+
"deploySchema": {
|
|
99
|
+
"role": "deploySchema",
|
|
100
|
+
"$prototype": "Function",
|
|
101
|
+
"access": "public",
|
|
102
|
+
"scope": "static",
|
|
103
|
+
"identifier": "deploySchema",
|
|
104
|
+
"timing": ["compiler", "server"],
|
|
105
|
+
"parameters": [
|
|
106
|
+
{
|
|
107
|
+
"identifier": "sectionValue",
|
|
108
|
+
"type": { "type": "object" },
|
|
109
|
+
"description": "The project.json `auth` section value"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"identifier": "projectConfig",
|
|
113
|
+
"type": { "type": "object" },
|
|
114
|
+
"description": "The full project config (connection lookup)"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"identifier": "options",
|
|
118
|
+
"type": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"env": { "type": "object" },
|
|
122
|
+
"dryRun": { "type": "boolean" },
|
|
123
|
+
"connection": { "type": "string" },
|
|
124
|
+
"connectors": { "type": "object" }
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"description": "Push options: env, dry-run, optional connection filter, provider stand-ins"
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"returnType": { "type": "object" },
|
|
131
|
+
"description": "Additive Better Auth system-table sync: { statements, applied, warnings, connection }"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jxsuite.com/schemas/auth/AuthActions.class.json",
|
|
4
|
+
"title": "AuthActions",
|
|
5
|
+
"description": "Form-friendly auth handlers: resolves to { signInEmail, signUpEmail, signInSocial, signOut }, each (scope, event)-callable reading the enclosing form's FormData, bumping the session store, and applying the configured redirects",
|
|
6
|
+
"$prototype": "Class",
|
|
7
|
+
"extends": "Object",
|
|
8
|
+
"$implementation": "./actions.js",
|
|
9
|
+
|
|
10
|
+
"$studio": {
|
|
11
|
+
"stateDefaults": { "timing": "client" }
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"$defs": {
|
|
15
|
+
"parameters": {
|
|
16
|
+
"redirects": {
|
|
17
|
+
"identifier": "redirects",
|
|
18
|
+
"type": { "type": "object" },
|
|
19
|
+
"description": "{ afterSignIn?, afterSignOut? } paths (defaults to the project auth section's redirects)"
|
|
20
|
+
},
|
|
21
|
+
"provider": {
|
|
22
|
+
"identifier": "provider",
|
|
23
|
+
"type": { "type": "string" },
|
|
24
|
+
"description": "Default social provider id for signInSocial (a form field or the def may override)"
|
|
25
|
+
},
|
|
26
|
+
"baseUrl": {
|
|
27
|
+
"identifier": "baseUrl",
|
|
28
|
+
"type": { "type": "string" },
|
|
29
|
+
"description": "Auth route base URL (defaults to <origin>/_jx/auth)"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"constructor": {
|
|
33
|
+
"role": "constructor",
|
|
34
|
+
"parameters": [
|
|
35
|
+
{ "$ref": "#/$defs/parameters/redirects" },
|
|
36
|
+
{ "$ref": "#/$defs/parameters/provider" },
|
|
37
|
+
{ "$ref": "#/$defs/parameters/baseUrl" }
|
|
38
|
+
],
|
|
39
|
+
"body": ["this.config = config;"]
|
|
40
|
+
},
|
|
41
|
+
"methods": {
|
|
42
|
+
"resolve": {
|
|
43
|
+
"role": "method",
|
|
44
|
+
"scope": "instance",
|
|
45
|
+
"identifier": "resolve",
|
|
46
|
+
"returnType": { "type": "object" },
|
|
47
|
+
"description": "The handler map: signInEmail / signUpEmail / signInSocial / signOut, wired via $ref (e.g. onsubmit: { \"$ref\": \"auth.signInEmail\" })"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jxsuite.com/schemas/auth/Session.class.json",
|
|
4
|
+
"title": "Session",
|
|
5
|
+
"description": "The signed-in user's session as reactive state: resolves to { userId, role?, user } or null, updating live as auth actions sign the user in and out. Resolves to null outside browsers, so statically generated pages render the signed-out state",
|
|
6
|
+
"$prototype": "Class",
|
|
7
|
+
"extends": "Object",
|
|
8
|
+
"$implementation": "./session.js",
|
|
9
|
+
|
|
10
|
+
"$studio": {
|
|
11
|
+
"stateDefaults": { "timing": "client" }
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"$defs": {
|
|
15
|
+
"parameters": {
|
|
16
|
+
"baseUrl": {
|
|
17
|
+
"identifier": "baseUrl",
|
|
18
|
+
"type": { "type": "string" },
|
|
19
|
+
"description": "Auth route base URL (defaults to <origin>/_jx/auth)"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"constructor": {
|
|
23
|
+
"role": "constructor",
|
|
24
|
+
"parameters": [{ "$ref": "#/$defs/parameters/baseUrl" }],
|
|
25
|
+
"body": ["this.config = config;"]
|
|
26
|
+
},
|
|
27
|
+
"methods": {
|
|
28
|
+
"resolve": {
|
|
29
|
+
"role": "method",
|
|
30
|
+
"scope": "instance",
|
|
31
|
+
"identifier": "resolve",
|
|
32
|
+
"returnType": { "type": ["object", "null"] },
|
|
33
|
+
"description": "SessionInfo { userId, role?, user } for the signed-in user, or null (always null outside browsers)"
|
|
34
|
+
},
|
|
35
|
+
"subscribe": {
|
|
36
|
+
"role": "method",
|
|
37
|
+
"scope": "instance",
|
|
38
|
+
"identifier": "subscribe",
|
|
39
|
+
"parameters": [
|
|
40
|
+
{
|
|
41
|
+
"identifier": "callback",
|
|
42
|
+
"type": { "type": "object" },
|
|
43
|
+
"description": "Called with the new SessionInfo|null whenever the session store changes"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"description": "Subscribe to session changes (auth actions bump the store after sign-in/out)"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actions — the `AuthActions` state class: form-friendly sign-in/sign-up/sign-out handlers.
|
|
3
|
+
*
|
|
4
|
+
* `resolve()` returns `{ signInEmail, signUpEmail, signInSocial, signOut }`; each handler is
|
|
5
|
+
* `(scope, event)`-callable — the same shape the connector's table actions use — reading the
|
|
6
|
+
* submitting form's FormData, calling the Better Auth client, refreshing the session store, and
|
|
7
|
+
* applying the configured redirects. Successful calls also bump `scope._v`, re-running lowered
|
|
8
|
+
* table queries whose visible rows may change with the auth state (owner-scoped reads). Wire via
|
|
9
|
+
* `"onsubmit": { "$ref": "auth.signInEmail" }` on a form with email/password fields.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getAuthClient } from "./client.ts";
|
|
13
|
+
import { clearSession, fetchSession } from "./session.ts";
|
|
14
|
+
import type { AuthClientResult } from "./client.ts";
|
|
15
|
+
|
|
16
|
+
/** Redirect paths applied after auth state changes. */
|
|
17
|
+
export interface AuthRedirects {
|
|
18
|
+
afterSignIn?: string;
|
|
19
|
+
afterSignOut?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Config of the AuthActions state class. */
|
|
23
|
+
export interface AuthActionsConfig {
|
|
24
|
+
redirects?: AuthRedirects;
|
|
25
|
+
/** Default social provider id for signInSocial (a form field named "provider" overrides). */
|
|
26
|
+
provider?: string;
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
/** Injected by hosts (compiler/dev server): the project sections, including `auth`. */
|
|
29
|
+
_project?: { auth?: { redirects?: AuthRedirects }; [key: string]: unknown };
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Scope = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
/** A `(scope, event)`-callable auth handler resolving true when the call succeeded. */
|
|
36
|
+
export type AuthActionHandler = (scope: Scope, event?: Event) => Promise<boolean>;
|
|
37
|
+
|
|
38
|
+
/** The submitting form: the event target itself, or the target control's owner form. */
|
|
39
|
+
function formOf(event?: Event): HTMLFormElement | null {
|
|
40
|
+
const target = event?.target as (HTMLElement & { form?: HTMLFormElement | null }) | null;
|
|
41
|
+
if (!target) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return target.tagName === "FORM" ? (target as HTMLFormElement) : (target.form ?? null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** String field values of the submitting form (files are ignored). */
|
|
48
|
+
function formValues(event?: Event): Record<string, string> {
|
|
49
|
+
const form = formOf(event);
|
|
50
|
+
if (!form) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
const out: Record<string, string> = {};
|
|
54
|
+
for (const [key, value] of new FormData(form)) {
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
out[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Navigate to a redirect path when one is configured (browser only). */
|
|
63
|
+
function navigate(path: string | undefined): void {
|
|
64
|
+
if (path && typeof location !== "undefined") {
|
|
65
|
+
location.assign(path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Bump the read-after-write version so owner-scoped queries re-run under the new session. */
|
|
70
|
+
function bumpVersion(scope: Scope): void {
|
|
71
|
+
scope._v = ((scope._v as number) || 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class AuthActions {
|
|
75
|
+
config: AuthActionsConfig;
|
|
76
|
+
|
|
77
|
+
constructor(config: AuthActionsConfig = {}) {
|
|
78
|
+
this.config = config;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Effective redirects: the def's own, else the project auth section's. */
|
|
82
|
+
redirects(): AuthRedirects {
|
|
83
|
+
return this.config.redirects ?? this.config._project?.auth?.redirects ?? {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The handler map, wired via $ref (e.g. `onsubmit: { "$ref": "auth.signInEmail" }`). */
|
|
87
|
+
resolve(): Record<string, AuthActionHandler> {
|
|
88
|
+
return {
|
|
89
|
+
signInEmail: this.#authCall((values, client) =>
|
|
90
|
+
client.signIn.email({ email: values.email ?? "", password: values.password ?? "" }),
|
|
91
|
+
),
|
|
92
|
+
signInSocial: this.#authCall(
|
|
93
|
+
(values, client) => {
|
|
94
|
+
const provider = values.provider ?? this.config.provider ?? "";
|
|
95
|
+
const callbackURL = this.redirects().afterSignIn;
|
|
96
|
+
return client.signIn.social({
|
|
97
|
+
provider,
|
|
98
|
+
...(callbackURL === undefined ? {} : { callbackURL }),
|
|
99
|
+
});
|
|
100
|
+
// Social sign-in redirects the whole page itself — no local navigation.
|
|
101
|
+
},
|
|
102
|
+
{ redirect: false },
|
|
103
|
+
),
|
|
104
|
+
signOut: async (scope: Scope, event?: Event): Promise<boolean> => {
|
|
105
|
+
event?.preventDefault?.();
|
|
106
|
+
const { error } = await getAuthClient(this.config.baseUrl).signOut();
|
|
107
|
+
if (error) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
clearSession();
|
|
111
|
+
bumpVersion(scope);
|
|
112
|
+
navigate(this.redirects().afterSignOut);
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
signUpEmail: this.#authCall((values, client) =>
|
|
116
|
+
client.signUp.email({
|
|
117
|
+
email: values.email ?? "",
|
|
118
|
+
// Better Auth requires a name; fall back to the email's local part.
|
|
119
|
+
name: values.name ?? (values.email ?? "").split("@")[0] ?? "",
|
|
120
|
+
password: values.password ?? "",
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Build a form-reading handler around one auth client call. */
|
|
127
|
+
#authCall(
|
|
128
|
+
call: (
|
|
129
|
+
values: Record<string, string>,
|
|
130
|
+
client: ReturnType<typeof getAuthClient>,
|
|
131
|
+
) => Promise<AuthClientResult>,
|
|
132
|
+
options: { redirect?: boolean } = {},
|
|
133
|
+
): AuthActionHandler {
|
|
134
|
+
return async (scope: Scope, event?: Event): Promise<boolean> => {
|
|
135
|
+
event?.preventDefault?.();
|
|
136
|
+
const client = getAuthClient(this.config.baseUrl);
|
|
137
|
+
const { error } = await call(formValues(event), client);
|
|
138
|
+
if (error) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
await fetchSession(this.config.baseUrl);
|
|
142
|
+
bumpVersion(scope);
|
|
143
|
+
if (options.redirect !== false) {
|
|
144
|
+
navigate(this.redirects().afterSignIn);
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|