@pattern-js/mod-identity 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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/admin-routes.d.ts +30 -0
- package/dist/admin-routes.d.ts.map +1 -0
- package/dist/admin-routes.js +52 -0
- package/dist/admin-routes.js.map +1 -0
- package/dist/auth-provider.d.ts +16 -0
- package/dist/auth-provider.d.ts.map +1 -0
- package/dist/auth-provider.js +47 -0
- package/dist/auth-provider.js.map +1 -0
- package/dist/cookies.d.ts +19 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +44 -0
- package/dist/cookies.js.map +1 -0
- package/dist/csrf.d.ts +19 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +39 -0
- package/dist/csrf.js.map +1 -0
- package/dist/deliver.d.ts +28 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +36 -0
- package/dist/deliver.js.map +1 -0
- package/dist/frontend.d.ts +10 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +139 -0
- package/dist/frontend.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/mod.d.ts +21 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +97 -0
- package/dist/mod.js.map +1 -0
- package/dist/ops.d.ts +26 -0
- package/dist/ops.d.ts.map +1 -0
- package/dist/ops.js +379 -0
- package/dist/ops.js.map +1 -0
- package/dist/options.d.ts +53 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +22 -0
- package/dist/options.js.map +1 -0
- package/dist/pages/bootstrap.d.ts +13 -0
- package/dist/pages/bootstrap.d.ts.map +1 -0
- package/dist/pages/bootstrap.js +23 -0
- package/dist/pages/bootstrap.js.map +1 -0
- package/dist/pages/html.d.ts +16 -0
- package/dist/pages/html.d.ts.map +1 -0
- package/dist/pages/html.js +72 -0
- package/dist/pages/html.js.map +1 -0
- package/dist/pages/login.d.ts +17 -0
- package/dist/pages/login.d.ts.map +1 -0
- package/dist/pages/login.js +43 -0
- package/dist/pages/login.js.map +1 -0
- package/dist/pages/welcome.d.ts +13 -0
- package/dist/pages/welcome.d.ts.map +1 -0
- package/dist/pages/welcome.js +19 -0
- package/dist/pages/welcome.js.map +1 -0
- package/dist/service-key.d.ts +12 -0
- package/dist/service-key.d.ts.map +1 -0
- package/dist/service-key.js +16 -0
- package/dist/service-key.js.map +1 -0
- package/dist/service.d.ts +133 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +208 -0
- package/dist/service.js.map +1 -0
- package/dist/store/memory.d.ts +11 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +0 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/migrations.d.ts +21 -0
- package/dist/store/migrations.d.ts.map +1 -0
- package/dist/store/migrations.js +87 -0
- package/dist/store/migrations.js.map +1 -0
- package/dist/store/mutex.d.ts +13 -0
- package/dist/store/mutex.d.ts.map +1 -0
- package/dist/store/mutex.js +28 -0
- package/dist/store/mutex.js.map +1 -0
- package/dist/store/sqlite.d.ts +20 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/store/sqlite.js +254 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/store/types.d.ts +135 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +23 -0
- package/dist/store/types.js.map +1 -0
- package/dist/tokens.d.ts +16 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +25 -0
- package/dist/tokens.js.map +1 -0
- package/dist/workflows.d.ts +14 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +96 -0
- package/dist/workflows.js.map +1 -0
- package/docs/index.md +162 -0
- package/docs/ops/identity.bootstrap.page.md +5 -0
- package/docs/ops/identity.bootstrap.submit.md +5 -0
- package/docs/ops/identity.login.page.md +5 -0
- package/docs/ops/identity.logout.md +4 -0
- package/docs/ops/identity.sessions.list.md +5 -0
- package/docs/ops/identity.sessions.revoke.md +4 -0
- package/docs/ops/identity.settings.get.md +4 -0
- package/docs/ops/identity.settings.set.md +5 -0
- package/docs/ops/identity.token.callback.md +7 -0
- package/docs/ops/identity.users.get.md +5 -0
- package/docs/ops/identity.users.invite.md +7 -0
- package/docs/ops/identity.users.list.md +5 -0
- package/docs/ops/identity.users.loginLink.md +5 -0
- package/docs/ops/identity.users.revokeSessions.md +5 -0
- package/docs/ops/identity.users.runStats.md +7 -0
- package/docs/ops/identity.users.setRoles.md +5 -0
- package/docs/ops/identity.users.toggleDisabled.md +5 -0
- package/docs/ops/identity.welcome.page.md +5 -0
- package/docs/ops/identity.whoami.md +6 -0
- package/package.json +45 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — node:sqlite store driver.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written SQL, no ORM (decided): the schema is five boring tables and the
|
|
5
|
+
* security-critical statements should read as literal SQL. CAS is a single
|
|
6
|
+
* conditional UPDATE — `WHERE id=? AND version=?` (+ `consumed_at IS NULL` for
|
|
7
|
+
* tokens), checked via `changes === 1` — which is atomic in SQLite and ports
|
|
8
|
+
* verbatim to Postgres, so a future driver is a file, not a redesign.
|
|
9
|
+
*
|
|
10
|
+
* `node:sqlite` is imported dynamically with a helpful error (same pattern as
|
|
11
|
+
* runtime-node's sqlite trace sink); the in-memory store covers builds
|
|
12
|
+
* without it.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { UniqueViolationError, } from "./types.js";
|
|
17
|
+
import { runMigrations } from "./migrations.js";
|
|
18
|
+
const toUser = (r) => ({
|
|
19
|
+
id: String(r.id),
|
|
20
|
+
email: String(r.email),
|
|
21
|
+
emailNorm: String(r.email_norm),
|
|
22
|
+
name: r.name ?? null,
|
|
23
|
+
roles: JSON.parse(String(r.roles ?? "[]")),
|
|
24
|
+
disabled: Number(r.disabled) === 1,
|
|
25
|
+
version: Number(r.version),
|
|
26
|
+
createdAt: Number(r.created_at),
|
|
27
|
+
updatedAt: Number(r.updated_at),
|
|
28
|
+
});
|
|
29
|
+
const toSession = (r) => ({
|
|
30
|
+
id: String(r.id),
|
|
31
|
+
tokenHash: String(r.token_hash),
|
|
32
|
+
userId: String(r.user_id),
|
|
33
|
+
createdAt: Number(r.created_at),
|
|
34
|
+
lastSeenAt: Number(r.last_seen_at),
|
|
35
|
+
expiresAt: Number(r.expires_at),
|
|
36
|
+
revokedAt: r.revoked_at == null ? null : Number(r.revoked_at),
|
|
37
|
+
version: Number(r.version),
|
|
38
|
+
userAgent: r.user_agent ?? null,
|
|
39
|
+
ip: r.ip ?? null,
|
|
40
|
+
});
|
|
41
|
+
const toToken = (r) => ({
|
|
42
|
+
id: String(r.id),
|
|
43
|
+
tokenHash: String(r.token_hash),
|
|
44
|
+
purpose: String(r.purpose),
|
|
45
|
+
emailNorm: r.email_norm ?? null,
|
|
46
|
+
data: r.data == null ? null : JSON.parse(String(r.data)),
|
|
47
|
+
createdAt: Number(r.created_at),
|
|
48
|
+
expiresAt: Number(r.expires_at),
|
|
49
|
+
consumedAt: r.consumed_at == null ? null : Number(r.consumed_at),
|
|
50
|
+
version: Number(r.version),
|
|
51
|
+
});
|
|
52
|
+
/* ── stores ────────────────────────────────────────────────────────────── */
|
|
53
|
+
class SqliteUserStore {
|
|
54
|
+
db;
|
|
55
|
+
constructor(db) {
|
|
56
|
+
this.db = db;
|
|
57
|
+
}
|
|
58
|
+
async countUsers() {
|
|
59
|
+
const r = this.db.prepare("SELECT COUNT(*) AS n FROM users").get();
|
|
60
|
+
return Number(r.n);
|
|
61
|
+
}
|
|
62
|
+
async findById(id) {
|
|
63
|
+
const r = this.db.prepare("SELECT * FROM users WHERE id = ?").get(id);
|
|
64
|
+
return r ? toUser(r) : null;
|
|
65
|
+
}
|
|
66
|
+
async findByEmailNorm(emailNorm) {
|
|
67
|
+
const r = this.db.prepare("SELECT * FROM users WHERE email_norm = ?").get(emailNorm);
|
|
68
|
+
return r ? toUser(r) : null;
|
|
69
|
+
}
|
|
70
|
+
async findByIdentity(provider, subject) {
|
|
71
|
+
const r = this.db
|
|
72
|
+
.prepare("SELECT u.* FROM users u JOIN identities i ON i.user_id = u.id WHERE i.provider = ? AND i.subject = ?")
|
|
73
|
+
.get(provider, subject);
|
|
74
|
+
return r ? toUser(r) : null;
|
|
75
|
+
}
|
|
76
|
+
async createUser(input) {
|
|
77
|
+
const id = crypto.randomUUID();
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
this.db.exec("BEGIN");
|
|
80
|
+
try {
|
|
81
|
+
this.db
|
|
82
|
+
.prepare("INSERT INTO users (id, email, email_norm, name, roles, disabled, version, created_at, updated_at) VALUES (?,?,?,?,?,0,1,?,?)")
|
|
83
|
+
.run(id, input.email, input.emailNorm, input.name ?? null, JSON.stringify(input.roles), now, now);
|
|
84
|
+
this.db
|
|
85
|
+
.prepare("INSERT INTO identities (provider, subject, user_id, created_at) VALUES (?,?,?,?)")
|
|
86
|
+
.run(input.identity.provider, input.identity.subject, id, now);
|
|
87
|
+
this.db.exec("COMMIT");
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
this.db.exec("ROLLBACK");
|
|
91
|
+
if (String(err).includes("UNIQUE"))
|
|
92
|
+
throw new UniqueViolationError("email");
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
return (await this.findById(id));
|
|
96
|
+
}
|
|
97
|
+
async linkIdentity(userId, provider, subject) {
|
|
98
|
+
this.db
|
|
99
|
+
.prepare("INSERT OR IGNORE INTO identities (provider, subject, user_id, created_at) VALUES (?,?,?,?)")
|
|
100
|
+
.run(provider, subject, userId, Date.now());
|
|
101
|
+
}
|
|
102
|
+
async updateUser(id, expectedVersion, patch) {
|
|
103
|
+
const current = await this.findById(id);
|
|
104
|
+
if (!current)
|
|
105
|
+
return null;
|
|
106
|
+
const next = { ...current, ...patch };
|
|
107
|
+
const info = this.db
|
|
108
|
+
.prepare("UPDATE users SET name=?, roles=?, disabled=?, version=version+1, updated_at=? WHERE id=? AND version=?")
|
|
109
|
+
.run(next.name, JSON.stringify(next.roles), next.disabled ? 1 : 0, Date.now(), id, expectedVersion);
|
|
110
|
+
return Number(info.changes) === 1 ? this.findById(id) : null;
|
|
111
|
+
}
|
|
112
|
+
async listUsers(opts = {}) {
|
|
113
|
+
const rows = this.db
|
|
114
|
+
.prepare("SELECT * FROM users ORDER BY created_at ASC LIMIT ? OFFSET ?")
|
|
115
|
+
.all(opts.limit ?? 500, opts.offset ?? 0);
|
|
116
|
+
return rows.map(toUser);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
class SqliteSessionStore {
|
|
120
|
+
db;
|
|
121
|
+
constructor(db) {
|
|
122
|
+
this.db = db;
|
|
123
|
+
}
|
|
124
|
+
async create(row) {
|
|
125
|
+
this.db
|
|
126
|
+
.prepare("INSERT INTO sessions (id, token_hash, user_id, created_at, last_seen_at, expires_at, revoked_at, version, user_agent, ip) VALUES (?,?,?,?,?,?,?,1,?,?)")
|
|
127
|
+
.run(row.id, row.tokenHash, row.userId, row.createdAt, row.lastSeenAt, row.expiresAt, row.revokedAt, row.userAgent, row.ip);
|
|
128
|
+
return (await this.findById(row.id));
|
|
129
|
+
}
|
|
130
|
+
async findByTokenHash(tokenHash) {
|
|
131
|
+
const r = this.db.prepare("SELECT * FROM sessions WHERE token_hash = ?").get(tokenHash);
|
|
132
|
+
return r ? toSession(r) : null;
|
|
133
|
+
}
|
|
134
|
+
async findById(id) {
|
|
135
|
+
const r = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
136
|
+
return r ? toSession(r) : null;
|
|
137
|
+
}
|
|
138
|
+
async touch(id, expectedVersion, lastSeenAt, expiresAt) {
|
|
139
|
+
const info = this.db
|
|
140
|
+
.prepare("UPDATE sessions SET last_seen_at=?, expires_at=?, version=version+1 WHERE id=? AND version=?")
|
|
141
|
+
.run(lastSeenAt, expiresAt, id, expectedVersion);
|
|
142
|
+
return Number(info.changes) === 1 ? this.findById(id) : null;
|
|
143
|
+
}
|
|
144
|
+
async rotate(id, expectedVersion, newTokenHash) {
|
|
145
|
+
const info = this.db
|
|
146
|
+
.prepare("UPDATE sessions SET token_hash=?, version=version+1 WHERE id=? AND version=?")
|
|
147
|
+
.run(newTokenHash, id, expectedVersion);
|
|
148
|
+
return Number(info.changes) === 1 ? this.findById(id) : null;
|
|
149
|
+
}
|
|
150
|
+
async revoke(id, at) {
|
|
151
|
+
this.db.prepare("UPDATE sessions SET revoked_at=?, version=version+1 WHERE id=? AND revoked_at IS NULL").run(at, id);
|
|
152
|
+
}
|
|
153
|
+
async revokeAllForUser(userId, at) {
|
|
154
|
+
const ids = this.db.prepare("SELECT id FROM sessions WHERE user_id=? AND revoked_at IS NULL").all(userId).map((r) => String(r.id));
|
|
155
|
+
this.db
|
|
156
|
+
.prepare("UPDATE sessions SET revoked_at=?, version=version+1 WHERE user_id=? AND revoked_at IS NULL")
|
|
157
|
+
.run(at, userId);
|
|
158
|
+
return ids;
|
|
159
|
+
}
|
|
160
|
+
async listForUser(userId) {
|
|
161
|
+
const rows = this.db
|
|
162
|
+
.prepare("SELECT * FROM sessions WHERE user_id=? ORDER BY created_at DESC")
|
|
163
|
+
.all(userId);
|
|
164
|
+
return rows.map(toSession);
|
|
165
|
+
}
|
|
166
|
+
async listAll(opts = {}) {
|
|
167
|
+
const rows = this.db
|
|
168
|
+
.prepare("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?")
|
|
169
|
+
.all(opts.limit ?? 500);
|
|
170
|
+
return rows.map(toSession);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class SqliteTokenStore {
|
|
174
|
+
db;
|
|
175
|
+
constructor(db) {
|
|
176
|
+
this.db = db;
|
|
177
|
+
}
|
|
178
|
+
async create(row) {
|
|
179
|
+
this.db
|
|
180
|
+
.prepare("INSERT INTO tokens (id, token_hash, purpose, email_norm, data, created_at, expires_at, consumed_at, version) VALUES (?,?,?,?,?,?,?,?,1)")
|
|
181
|
+
.run(row.id, row.tokenHash, row.purpose, row.emailNorm, row.data == null ? null : JSON.stringify(row.data), row.createdAt, row.expiresAt, row.consumedAt);
|
|
182
|
+
return (await this.findById(row.id));
|
|
183
|
+
}
|
|
184
|
+
async findById(id) {
|
|
185
|
+
const r = this.db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
186
|
+
return r ? toToken(r) : null;
|
|
187
|
+
}
|
|
188
|
+
async findByTokenHash(tokenHash) {
|
|
189
|
+
const r = this.db.prepare("SELECT * FROM tokens WHERE token_hash = ?").get(tokenHash);
|
|
190
|
+
return r ? toToken(r) : null;
|
|
191
|
+
}
|
|
192
|
+
async consume(id, expectedVersion, at) {
|
|
193
|
+
// The load-bearing CAS: `consumed_at IS NULL` in the WHERE means exactly
|
|
194
|
+
// one of N concurrent consumers sees changes === 1.
|
|
195
|
+
const info = this.db
|
|
196
|
+
.prepare("UPDATE tokens SET consumed_at=?, version=version+1 WHERE id=? AND version=? AND consumed_at IS NULL")
|
|
197
|
+
.run(at, id, expectedVersion);
|
|
198
|
+
return Number(info.changes) === 1 ? this.findById(id) : null;
|
|
199
|
+
}
|
|
200
|
+
async deleteExpired(now) {
|
|
201
|
+
const info = this.db.prepare("DELETE FROM tokens WHERE expires_at < ?").run(now);
|
|
202
|
+
return Number(info.changes);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
class SqliteSettingsStore {
|
|
206
|
+
db;
|
|
207
|
+
constructor(db) {
|
|
208
|
+
this.db = db;
|
|
209
|
+
}
|
|
210
|
+
async get(key) {
|
|
211
|
+
const r = this.db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
|
|
212
|
+
return r ? String(r.value) : null;
|
|
213
|
+
}
|
|
214
|
+
async set(key, value) {
|
|
215
|
+
this.db
|
|
216
|
+
.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
217
|
+
.run(key, value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/* ── factory ───────────────────────────────────────────────────────────── */
|
|
221
|
+
/**
|
|
222
|
+
* Open (or create) the identity database at `filePath` (":memory:" works too).
|
|
223
|
+
* Throws a helpful error when this Node build lacks `node:sqlite`.
|
|
224
|
+
*/
|
|
225
|
+
export async function sqliteIdentityStores(filePath) {
|
|
226
|
+
// process.getBuiltinModule (Node ≥22.3) loads the optional builtin without
|
|
227
|
+
// going through the module graph — bundlers and vite-node leave it alone.
|
|
228
|
+
let DatabaseSync;
|
|
229
|
+
try {
|
|
230
|
+
DatabaseSync = process.getBuiltinModule("node:sqlite")
|
|
231
|
+
?.DatabaseSync;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
/* fall through to the helpful error */
|
|
235
|
+
}
|
|
236
|
+
if (!DatabaseSync) {
|
|
237
|
+
throw new Error("node:sqlite is not available in this Node build — @pattern-js/mod-identity needs Node ≥22.5 " +
|
|
238
|
+
"(≥24 recommended). For tests, use memoryIdentityStores().");
|
|
239
|
+
}
|
|
240
|
+
if (filePath !== ":memory:")
|
|
241
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
242
|
+
const db = new DatabaseSync(filePath);
|
|
243
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
244
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
245
|
+
runMigrations(db);
|
|
246
|
+
return {
|
|
247
|
+
users: new SqliteUserStore(db),
|
|
248
|
+
sessions: new SqliteSessionStore(db),
|
|
249
|
+
tokens: new SqliteTokenStore(db),
|
|
250
|
+
settings: new SqliteSettingsStore(db),
|
|
251
|
+
close: async () => db.close(),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/store/sqlite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — store contracts.
|
|
3
|
+
*
|
|
4
|
+
* Three small stores behind interfaces so the persistence layer is swappable
|
|
5
|
+
* (sqlite ships built-in, in-memory for tests, Postgres later as a driver).
|
|
6
|
+
* Every mutation that can race is **compare-and-swap**: rows carry a `version`,
|
|
7
|
+
* writers pass the version they read, and a `null` return means "lost the
|
|
8
|
+
* race — re-read and retry". On SQL backends CAS is a single conditional
|
|
9
|
+
* UPDATE, which is what makes single-use tokens and session rotation hold
|
|
10
|
+
* across multiple instances; the in-process mutex on top only trims redundant
|
|
11
|
+
* retries on a single node.
|
|
12
|
+
*
|
|
13
|
+
* Secrets never hit storage: sessions and tokens store the sha256 of the
|
|
14
|
+
* opaque secret, so a leaked database can't impersonate anyone.
|
|
15
|
+
*/
|
|
16
|
+
export type TokenPurpose = "login" | "invite" | "bootstrap";
|
|
17
|
+
export interface UserRow {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
/** lower(trim(email)) — the uniqueness key. */
|
|
21
|
+
emailNorm: string;
|
|
22
|
+
name: string | null;
|
|
23
|
+
roles: string[];
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
version: number;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
}
|
|
29
|
+
export interface IdentityLink {
|
|
30
|
+
provider: string;
|
|
31
|
+
/** Provider-scoped subject (for magic-link this is the emailNorm). */
|
|
32
|
+
subject: string;
|
|
33
|
+
userId: string;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SessionRow {
|
|
37
|
+
/** Stable session id (NOT the secret) — safe in principals, rooms, logs. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** sha256 hex of the opaque cookie token. */
|
|
40
|
+
tokenHash: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
/** Sliding-expiry bookkeeping; touches are throttled by the service. */
|
|
44
|
+
lastSeenAt: number;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
revokedAt: number | null;
|
|
47
|
+
version: number;
|
|
48
|
+
userAgent: string | null;
|
|
49
|
+
ip: string | null;
|
|
50
|
+
}
|
|
51
|
+
export interface TokenRow {
|
|
52
|
+
id: string;
|
|
53
|
+
/** sha256 hex of the single-use secret. */
|
|
54
|
+
tokenHash: string;
|
|
55
|
+
purpose: TokenPurpose;
|
|
56
|
+
/** Target email (login/invite); null for an open bootstrap token. */
|
|
57
|
+
emailNorm: string | null;
|
|
58
|
+
/** Extra payload: roles to grant on invite, a `next` url, … */
|
|
59
|
+
data: Record<string, unknown> | null;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
/** Set exactly once via CAS — null means unconsumed. */
|
|
63
|
+
consumedAt: number | null;
|
|
64
|
+
version: number;
|
|
65
|
+
}
|
|
66
|
+
/** Thrown when an insert hits the one-user-per-email constraint. */
|
|
67
|
+
export declare class UniqueViolationError extends Error {
|
|
68
|
+
constructor(field: string);
|
|
69
|
+
}
|
|
70
|
+
export interface UserStore {
|
|
71
|
+
countUsers(): Promise<number>;
|
|
72
|
+
findById(id: string): Promise<UserRow | null>;
|
|
73
|
+
findByEmailNorm(emailNorm: string): Promise<UserRow | null>;
|
|
74
|
+
findByIdentity(provider: string, subject: string): Promise<UserRow | null>;
|
|
75
|
+
/** Insert user + identity link atomically. Throws UniqueViolationError on a taken email. */
|
|
76
|
+
createUser(input: {
|
|
77
|
+
email: string;
|
|
78
|
+
emailNorm: string;
|
|
79
|
+
name?: string | null;
|
|
80
|
+
roles: string[];
|
|
81
|
+
identity: {
|
|
82
|
+
provider: string;
|
|
83
|
+
subject: string;
|
|
84
|
+
};
|
|
85
|
+
}): Promise<UserRow>;
|
|
86
|
+
/** Link an existing user to a provider identity (idempotent). */
|
|
87
|
+
linkIdentity(userId: string, provider: string, subject: string): Promise<void>;
|
|
88
|
+
/** CAS update. Returns the new row, or null on version mismatch (re-read & retry). */
|
|
89
|
+
updateUser(id: string, expectedVersion: number, patch: Partial<Pick<UserRow, "name" | "roles" | "disabled">>): Promise<UserRow | null>;
|
|
90
|
+
listUsers(opts?: {
|
|
91
|
+
limit?: number;
|
|
92
|
+
offset?: number;
|
|
93
|
+
}): Promise<UserRow[]>;
|
|
94
|
+
}
|
|
95
|
+
export interface SessionStore {
|
|
96
|
+
create(row: Omit<SessionRow, "version">): Promise<SessionRow>;
|
|
97
|
+
findByTokenHash(tokenHash: string): Promise<SessionRow | null>;
|
|
98
|
+
findById(id: string): Promise<SessionRow | null>;
|
|
99
|
+
/** Sliding touch (CAS). Null on version mismatch — safe to ignore, the read was fresh. */
|
|
100
|
+
touch(id: string, expectedVersion: number, lastSeenAt: number, expiresAt: number): Promise<SessionRow | null>;
|
|
101
|
+
/** Swap the secret on privilege change (CAS): new hash, same session id. */
|
|
102
|
+
rotate(id: string, expectedVersion: number, newTokenHash: string): Promise<SessionRow | null>;
|
|
103
|
+
revoke(id: string, at: number): Promise<void>;
|
|
104
|
+
/** Revoke every active session of a user; returns the revoked ids. */
|
|
105
|
+
revokeAllForUser(userId: string, at: number): Promise<string[]>;
|
|
106
|
+
listForUser(userId: string): Promise<SessionRow[]>;
|
|
107
|
+
/** All sessions, newest first (admin screen). */
|
|
108
|
+
listAll(opts?: {
|
|
109
|
+
limit?: number;
|
|
110
|
+
}): Promise<SessionRow[]>;
|
|
111
|
+
}
|
|
112
|
+
export interface TokenStore {
|
|
113
|
+
create(row: Omit<TokenRow, "version">): Promise<TokenRow>;
|
|
114
|
+
findByTokenHash(tokenHash: string): Promise<TokenRow | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Single-use consume (CAS): sets consumed_at only when it is still null AND
|
|
117
|
+
* the version matches. Exactly one of N concurrent consumers gets the row;
|
|
118
|
+
* the rest get null.
|
|
119
|
+
*/
|
|
120
|
+
consume(id: string, expectedVersion: number, at: number): Promise<TokenRow | null>;
|
|
121
|
+
deleteExpired(now: number): Promise<number>;
|
|
122
|
+
}
|
|
123
|
+
/** Small persisted key/value bag for runtime-mutable identity settings (signup policy…). */
|
|
124
|
+
export interface SettingsStore {
|
|
125
|
+
get(key: string): Promise<string | null>;
|
|
126
|
+
set(key: string, value: string): Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
export interface IdentityStores {
|
|
129
|
+
users: UserStore;
|
|
130
|
+
sessions: SessionStore;
|
|
131
|
+
tokens: TokenStore;
|
|
132
|
+
settings: SettingsStore;
|
|
133
|
+
close(): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE5D,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,YAAY,CAAC;IACtB,qEAAqE;IACrE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,oEAAoE;AACpE,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,KAAK,EAAE,MAAM;CAI1B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC9C,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC5D,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC3E,4FAA4F;IAC5F,UAAU,CAAC,KAAK,EAAE;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,QAAQ,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KACjD,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,sFAAsF;IACtF,UAAU,CACR,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC,CAAC,GAC3D,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC3B,SAAS,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9D,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC/D,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACjD,0FAA0F;IAC1F,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC9G,4EAA4E;IAC5E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC9F,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,sEAAsE;IACtE,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACnD,iDAAiD;IACjD,OAAO,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAC7D;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACnF,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7C;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — store contracts.
|
|
3
|
+
*
|
|
4
|
+
* Three small stores behind interfaces so the persistence layer is swappable
|
|
5
|
+
* (sqlite ships built-in, in-memory for tests, Postgres later as a driver).
|
|
6
|
+
* Every mutation that can race is **compare-and-swap**: rows carry a `version`,
|
|
7
|
+
* writers pass the version they read, and a `null` return means "lost the
|
|
8
|
+
* race — re-read and retry". On SQL backends CAS is a single conditional
|
|
9
|
+
* UPDATE, which is what makes single-use tokens and session rotation hold
|
|
10
|
+
* across multiple instances; the in-process mutex on top only trims redundant
|
|
11
|
+
* retries on a single node.
|
|
12
|
+
*
|
|
13
|
+
* Secrets never hit storage: sessions and tokens store the sha256 of the
|
|
14
|
+
* opaque secret, so a leaked database can't impersonate anyone.
|
|
15
|
+
*/
|
|
16
|
+
/** Thrown when an insert hits the one-user-per-email constraint. */
|
|
17
|
+
export class UniqueViolationError extends Error {
|
|
18
|
+
constructor(field) {
|
|
19
|
+
super(`unique constraint violated: ${field}`);
|
|
20
|
+
this.name = "UniqueViolationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyDH,oEAAoE;AACpE,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,KAAa;QACvB,KAAK,CAAC,+BAA+B,KAAK,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF"}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — secret helpers.
|
|
3
|
+
*
|
|
4
|
+
* Secrets are 256-bit random, base64url on the wire, sha256 at rest. Lookup
|
|
5
|
+
* is by hash (an exact index hit), so no constant-time comparison is needed —
|
|
6
|
+
* there is nothing secret-shaped to compare against.
|
|
7
|
+
*/
|
|
8
|
+
/** A fresh 256-bit secret, base64url (cookie- and URL-safe). */
|
|
9
|
+
export declare function randomToken(): string;
|
|
10
|
+
/** sha256 hex — the only form a secret takes in storage. */
|
|
11
|
+
export declare function sha256hex(value: string): string;
|
|
12
|
+
/** Normalize an email for identity purposes. */
|
|
13
|
+
export declare function normalizeEmail(email: string): string;
|
|
14
|
+
/** Loose-but-useful email shape check (real validation is the delivery). */
|
|
15
|
+
export declare function looksLikeEmail(email: string): boolean;
|
|
16
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,gEAAgE;AAChE,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,4DAA4D;AAC5D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD"}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — secret helpers.
|
|
3
|
+
*
|
|
4
|
+
* Secrets are 256-bit random, base64url on the wire, sha256 at rest. Lookup
|
|
5
|
+
* is by hash (an exact index hit), so no constant-time comparison is needed —
|
|
6
|
+
* there is nothing secret-shaped to compare against.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
9
|
+
/** A fresh 256-bit secret, base64url (cookie- and URL-safe). */
|
|
10
|
+
export function randomToken() {
|
|
11
|
+
return randomBytes(32).toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
/** sha256 hex — the only form a secret takes in storage. */
|
|
14
|
+
export function sha256hex(value) {
|
|
15
|
+
return createHash("sha256").update(value).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
/** Normalize an email for identity purposes. */
|
|
18
|
+
export function normalizeEmail(email) {
|
|
19
|
+
return email.trim().toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
/** Loose-but-useful email shape check (real validation is the delivery). */
|
|
22
|
+
export function looksLikeEmail(email) {
|
|
23
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,gEAAgE;AAChE,MAAM,UAAU,WAAW;IACzB,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACpC,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — endpoint workflows.
|
|
3
|
+
*
|
|
4
|
+
* The auth surface IS a set of Pattern workflows (same self-reflection as the
|
|
5
|
+
* admin API): each route is `http.request → identity.<op> → http.response`.
|
|
6
|
+
* HTTP ops get the full status/headers/body wiring (cookies and redirects are
|
|
7
|
+
* the result); JSON ops wire a single body. All of these are public — the
|
|
8
|
+
* pages must be reachable logged-out, and the privileged surface is ops-only
|
|
9
|
+
* (admin-stamped invoke), not routes.
|
|
10
|
+
*/
|
|
11
|
+
import type { Workflow } from "@pattern-js/core";
|
|
12
|
+
/** The identity routes under `mount` (default /auth). */
|
|
13
|
+
export declare function endpointWorkflows(mount: string): Workflow[];
|
|
14
|
+
//# sourceMappingURL=workflows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAsFjD,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAW3D"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — endpoint workflows.
|
|
3
|
+
*
|
|
4
|
+
* The auth surface IS a set of Pattern workflows (same self-reflection as the
|
|
5
|
+
* admin API): each route is `http.request → identity.<op> → http.response`.
|
|
6
|
+
* HTTP ops get the full status/headers/body wiring (cookies and redirects are
|
|
7
|
+
* the result); JSON ops wire a single body. All of these are public — the
|
|
8
|
+
* pages must be reachable logged-out, and the privileged surface is ops-only
|
|
9
|
+
* (admin-stamped invoke), not routes.
|
|
10
|
+
*/
|
|
11
|
+
import { authOpRoutes } from "./ops.js";
|
|
12
|
+
function endpoint(spec) {
|
|
13
|
+
if (spec.shape === "http") {
|
|
14
|
+
// Auth pages + redirects: decompose the request into the PURE op's discrete
|
|
15
|
+
// ports (query/body fields + the user-agent), run it, and wire its
|
|
16
|
+
// { body, redirect, cookies, status } onto the out-gate. The op never sees
|
|
17
|
+
// HTTP — Set-Cookie / Location are produced from its domain outputs here.
|
|
18
|
+
const io = authOpRoutes[spec.op];
|
|
19
|
+
if (!io)
|
|
20
|
+
throw new Error(`identity route "${spec.id}": no auth I/O for op "${spec.op}"`);
|
|
21
|
+
const nodes = [
|
|
22
|
+
{ id: "in", op: "boundary.http.request", config: { method: spec.method, path: spec.path } },
|
|
23
|
+
{ id: "call", op: spec.op },
|
|
24
|
+
{ id: "ct", op: "core.const.object", config: { value: { "content-type": "text/html; charset=utf-8" } } },
|
|
25
|
+
{ id: "out", op: "boundary.http.response", config: { mode: "buffered" } },
|
|
26
|
+
];
|
|
27
|
+
const edges = [];
|
|
28
|
+
let wired = false;
|
|
29
|
+
if (io.query.length) {
|
|
30
|
+
wired = true;
|
|
31
|
+
nodes.push({ id: "ex_query", op: "core.object.extract", config: { keys: io.query } });
|
|
32
|
+
edges.push({ from: { node: "in", port: "query" }, to: { node: "ex_query", port: "object" } });
|
|
33
|
+
for (const k of io.query)
|
|
34
|
+
edges.push({ from: { node: "ex_query", port: k }, to: { node: "call", port: k } });
|
|
35
|
+
}
|
|
36
|
+
if (io.body.length) {
|
|
37
|
+
wired = true;
|
|
38
|
+
nodes.push({ id: "ex_body", op: "core.object.extract", config: { keys: io.body } });
|
|
39
|
+
edges.push({ from: { node: "in", port: "body" }, to: { node: "ex_body", port: "object" } });
|
|
40
|
+
for (const k of io.body)
|
|
41
|
+
edges.push({ from: { node: "ex_body", port: k }, to: { node: "call", port: k } });
|
|
42
|
+
}
|
|
43
|
+
if (io.userAgent) {
|
|
44
|
+
wired = true;
|
|
45
|
+
nodes.push({ id: "ex_ua", op: "core.object.get", config: { path: "user-agent" } });
|
|
46
|
+
edges.push({ from: { node: "in", port: "headers" }, to: { node: "ex_ua", port: "object" } });
|
|
47
|
+
edges.push({ from: { node: "ex_ua", port: "out" }, to: { node: "call", port: "userAgent" } });
|
|
48
|
+
}
|
|
49
|
+
if (!wired)
|
|
50
|
+
edges.push({ from: { node: "in", port: "out" }, to: { node: "call", port: "in" } });
|
|
51
|
+
edges.push({ from: { node: "call", port: "body" }, to: { node: "out", port: "body" } });
|
|
52
|
+
edges.push({ from: { node: "call", port: "redirect" }, to: { node: "out", port: "redirect" } });
|
|
53
|
+
edges.push({ from: { node: "call", port: "cookies" }, to: { node: "out", port: "cookies" } });
|
|
54
|
+
edges.push({ from: { node: "call", port: "status" }, to: { node: "out", port: "status" } });
|
|
55
|
+
edges.push({ from: { node: "ct", port: "out" }, to: { node: "out", port: "headers" } });
|
|
56
|
+
return { id: spec.id, name: `Identity · ${spec.method} ${spec.path}`, source: "code", nodes, edges };
|
|
57
|
+
}
|
|
58
|
+
// JSON route: decompose the query into the pure op's ports, run it, map its
|
|
59
|
+
// outcome with boundary.http.status (the op never sees HTTP).
|
|
60
|
+
const out = spec.out ?? "out";
|
|
61
|
+
const query = spec.query ?? [];
|
|
62
|
+
const nodes = [
|
|
63
|
+
{ id: "in", op: "boundary.http.request", config: { method: spec.method, path: spec.path } },
|
|
64
|
+
{ id: "call", op: spec.op },
|
|
65
|
+
{ id: "status", op: "boundary.http.status" },
|
|
66
|
+
{ id: "out", op: "boundary.http.response", config: { mode: "buffered" } },
|
|
67
|
+
];
|
|
68
|
+
const edges = [];
|
|
69
|
+
if (query.length) {
|
|
70
|
+
nodes.push({ id: "ex_query", op: "core.object.extract", config: { keys: query } });
|
|
71
|
+
edges.push({ from: { node: "in", port: "query" }, to: { node: "ex_query", port: "object" } });
|
|
72
|
+
for (const k of query)
|
|
73
|
+
edges.push({ from: { node: "ex_query", port: k }, to: { node: "call", port: k } });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
edges.push({ from: { node: "in", port: "out" }, to: { node: "call", port: "in" } });
|
|
77
|
+
}
|
|
78
|
+
edges.push({ from: { node: "call", port: out }, to: { node: "status", port: "result" } });
|
|
79
|
+
edges.push({ from: { node: "status", port: "status" }, to: { node: "out", port: "status" } });
|
|
80
|
+
edges.push({ from: { node: "status", port: "body" }, to: { node: "out", port: "body" } });
|
|
81
|
+
return { id: spec.id, name: `Identity · ${spec.method} ${spec.path}`, source: "code", nodes, edges };
|
|
82
|
+
}
|
|
83
|
+
/** The identity routes under `mount` (default /auth). */
|
|
84
|
+
export function endpointWorkflows(mount) {
|
|
85
|
+
const specs = [
|
|
86
|
+
{ id: "identity.route.login", method: "GET", path: `${mount}/login`, op: "identity.login.page", shape: "http" },
|
|
87
|
+
{ id: "identity.route.token", method: "GET", path: `${mount}/token`, op: "identity.token.callback", shape: "http" },
|
|
88
|
+
{ id: "identity.route.logout", method: "POST", path: `${mount}/logout`, op: "identity.logout", shape: "http" },
|
|
89
|
+
{ id: "identity.route.whoami", method: "GET", path: `${mount}/whoami`, op: "identity.whoami", shape: "json", out: "whoami" },
|
|
90
|
+
{ id: "identity.route.welcome", method: "GET", path: `${mount}/welcome`, op: "identity.welcome.page", shape: "http" },
|
|
91
|
+
{ id: "identity.route.bootstrap", method: "GET", path: `${mount}/bootstrap`, op: "identity.bootstrap.page", shape: "http" },
|
|
92
|
+
{ id: "identity.route.bootstrap.submit", method: "POST", path: `${mount}/bootstrap`, op: "identity.bootstrap.submit", shape: "http" },
|
|
93
|
+
];
|
|
94
|
+
return specs.map(endpoint);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=workflows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflows.js","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"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"}
|