@nxgt/janus-drizzle 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 +12 -3
- package/dist/index.js +41 -4
- package/dist/index.js.map +4 -4
- package/dist/tables.d.ts +93 -3
- package/dist/tables.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide/migrations.md +70 -4
- package/docs/guide/wiring.md +4 -1
- package/docs/roadmap.md +5 -0
- package/docs/troubleshooting.md +21 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -101,10 +101,10 @@ tables.
|
|
|
101
101
|
|
|
102
102
|
| Table | Keys and indexes |
|
|
103
103
|
| --- | --- |
|
|
104
|
-
| `users` | `id` · `(id, type)` unique, for the logins' foreign key · `(type, id)` for listing · a check that a password has both its hash and its date, or neither |
|
|
104
|
+
| `users` | `id` · `(id, type)` unique, for the logins' foreign key · `(type, id)` for listing · a check that a password has both its hash and its date, or neither · a check that a second factor (`second_factor_method`, `_secret`, `_confirmed_at`, `_last_step`) has its method and its secret, or none of the four · a check that the method is `totp` and the last step not negative |
|
|
105
105
|
| `logins` | primary key `(type, login)`: **a login is unique per user type** · `(user_id, type)` references the user, `on delete cascade` |
|
|
106
106
|
| `sessions` | `id` · `token_hash` unique · `user_id` · `expires_at`, for `collectExpired()` |
|
|
107
|
-
| `tokens` | `token_hash` · `user_id` · a check on `kind` |
|
|
107
|
+
| `tokens` | `token_hash` · `user_id` · a check on `kind` · `code_hash`, and `attempts`, `not null default 0`, checked `>= 0` |
|
|
108
108
|
| `relations` | one row per tuple, unique `nulls not distinct` over all six columns, subject first: the index `findObjects` pages · `(object_type, object_id, relation)` for one hop forwards |
|
|
109
109
|
|
|
110
110
|
`logins` is also a `text[]` on `users`, which the store reads back in order.
|
|
@@ -113,10 +113,19 @@ table plays that role, written in the same transaction as the user.
|
|
|
113
113
|
|
|
114
114
|
Every key column is `text collate "C"`, compared byte for byte as the port
|
|
115
115
|
requires. No secret is stored: sessions and tokens hold `sha256` of the secret,
|
|
116
|
-
|
|
116
|
+
passwords a self-describing hash, and `second_factor_secret` a TOTP secret the
|
|
117
|
+
store keeps byte for byte, which `@nxgt/janus` will seal with your
|
|
118
|
+
application's key before the store sees it, once the second factor ships.
|
|
117
119
|
|
|
118
120
|
## Traps
|
|
119
121
|
|
|
122
|
+
- **Upgrading to 0.2 needs a migration.** The tables gained columns for the
|
|
123
|
+
second factor and for attempts at a code. Deployed without them, every
|
|
124
|
+
query on `users` and `tokens` fails with `STORE_FAILED`, caused by
|
|
125
|
+
`column "second_factor_method" does not exist` (or `"code_hash"`).
|
|
126
|
+
Run `bunx drizzle-kit generate`, then `bunx drizzle-kit migrate`, before
|
|
127
|
+
deploying — with `--config` naming Janus's config if it has its own.
|
|
128
|
+
[Upgrading](docs/guide/migrations.md#upgrading) shows what it writes.
|
|
120
129
|
- **Your migrations create the tables, not this package.** The core never
|
|
121
130
|
manages a schema, and the stores create nothing per request. Without the
|
|
122
131
|
exports in your schema file, the first sign-up fails with `STORE_FAILED`,
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,10 @@ function defineJanusTables(options = {}) {
|
|
|
37
37
|
logins: text("logins").array().notNull(),
|
|
38
38
|
passwordHash: text("password_hash"),
|
|
39
39
|
passwordUpdatedAt: at("password_updated_at"),
|
|
40
|
+
secondFactorMethod: text("second_factor_method", { enum: ["totp"] }),
|
|
41
|
+
secondFactorSecret: text("second_factor_secret"),
|
|
42
|
+
secondFactorConfirmedAt: at("second_factor_confirmed_at"),
|
|
43
|
+
secondFactorLastStep: integer("second_factor_last_step"),
|
|
40
44
|
emailVerifiedAt: at("email_verified_at"),
|
|
41
45
|
version: integer("version").notNull(),
|
|
42
46
|
createdAt: at("created_at").notNull(),
|
|
@@ -44,7 +48,9 @@ function defineJanusTables(options = {}) {
|
|
|
44
48
|
}, (t) => [
|
|
45
49
|
unique("users_id_type_unique").on(t.id, t.type),
|
|
46
50
|
index("users_type_id").on(t.type, t.id),
|
|
47
|
-
check("users_password_whole", sql`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`)
|
|
51
|
+
check("users_password_whole", sql`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`),
|
|
52
|
+
check("users_second_factor_whole", sql`(${t.secondFactorMethod} is null) = (${t.secondFactorSecret} is null) and (${t.secondFactorMethod} is not null or (${t.secondFactorConfirmedAt} is null and ${t.secondFactorLastStep} is null))`),
|
|
53
|
+
check("users_second_factor_values", sql`${t.secondFactorMethod} in ('totp') and ${t.secondFactorLastStep} >= 0`)
|
|
48
54
|
]);
|
|
49
55
|
const logins = table("logins", {
|
|
50
56
|
type: key("type").notNull(),
|
|
@@ -74,14 +80,19 @@ function defineJanusTables(options = {}) {
|
|
|
74
80
|
]);
|
|
75
81
|
const tokens = table("tokens", {
|
|
76
82
|
tokenHash: key("token_hash").primaryKey(),
|
|
77
|
-
kind: text("kind", {
|
|
83
|
+
kind: text("kind", {
|
|
84
|
+
enum: ["verifyEmail", "resetPassword", "secondFactor", "signInCode"]
|
|
85
|
+
}).notNull(),
|
|
78
86
|
userId: key("user_id").notNull(),
|
|
79
87
|
address: text("address").notNull(),
|
|
88
|
+
codeHash: text("code_hash"),
|
|
89
|
+
attempts: integer("attempts").notNull().default(0),
|
|
80
90
|
expiresAt: at("expires_at").notNull(),
|
|
81
91
|
spentAt: at("spent_at"),
|
|
82
92
|
createdAt: at("created_at").notNull()
|
|
83
93
|
}, (t) => [
|
|
84
|
-
check("tokens_kind", sql`${t.kind} in ('verifyEmail', 'resetPassword')`),
|
|
94
|
+
check("tokens_kind", sql`${t.kind} in ('verifyEmail', 'resetPassword', 'secondFactor', 'signInCode')`),
|
|
95
|
+
check("tokens_attempts", sql`${t.attempts} >= 0`),
|
|
85
96
|
index("tokens_user_id").on(t.userId)
|
|
86
97
|
]);
|
|
87
98
|
const relations = table("relations", {
|
|
@@ -352,12 +363,20 @@ function tokenStore(db, tables) {
|
|
|
352
363
|
kind: before.kind,
|
|
353
364
|
userId: before.userId,
|
|
354
365
|
address: before.address,
|
|
366
|
+
codeHash: before.codeHash,
|
|
367
|
+
attempts: before.attempts,
|
|
355
368
|
expiresAt: before.expiresAt,
|
|
356
369
|
spentAt: before.spentAt,
|
|
357
370
|
createdAt: before.createdAt
|
|
358
371
|
});
|
|
359
372
|
return spent === undefined ? null : { ...spent, userId: spent.userId };
|
|
360
373
|
}),
|
|
374
|
+
countAttempt: (tokenHash, kind) => run$("countAttempt", async () => {
|
|
375
|
+
const match = and2(eq2(tables.tokens.tokenHash, tokenHash), eq2(tables.tokens.kind, kind));
|
|
376
|
+
const [counted] = await db.update(tables.tokens).set({ attempts: sql2`${tables.tokens.attempts} + 1` }).where(and2(match, isNull2(tables.tokens.spentAt))).returning();
|
|
377
|
+
const row = counted ?? (await db.select().from(tables.tokens).where(match))[0];
|
|
378
|
+
return row === undefined ? null : { ...row, userId: row.userId };
|
|
379
|
+
}),
|
|
361
380
|
deleteUserTokens: (userId) => run$("deleteUserTokens", async () => {
|
|
362
381
|
const deleted = await db.delete(tables.tokens).where(eq2(tables.tokens.userId, userId)).returning({ tokenHash: tables.tokens.tokenHash });
|
|
363
382
|
return deleted.length;
|
|
@@ -374,6 +393,7 @@ function toUserRow(record) {
|
|
|
374
393
|
logins: [...record.logins],
|
|
375
394
|
passwordHash: record.password?.hash ?? null,
|
|
376
395
|
passwordUpdatedAt: record.password?.updatedAt ?? null,
|
|
396
|
+
...toSecondFactorColumns(record.secondFactor),
|
|
377
397
|
emailVerifiedAt: record.emailVerifiedAt,
|
|
378
398
|
version: record.version,
|
|
379
399
|
createdAt: record.createdAt,
|
|
@@ -398,8 +418,19 @@ function toUserSet(patch) {
|
|
|
398
418
|
set.passwordHash = patch.password?.hash ?? null;
|
|
399
419
|
set.passwordUpdatedAt = patch.password?.updatedAt ?? null;
|
|
400
420
|
}
|
|
421
|
+
if (patch.secondFactor !== undefined) {
|
|
422
|
+
Object.assign(set, toSecondFactorColumns(patch.secondFactor));
|
|
423
|
+
}
|
|
401
424
|
return set;
|
|
402
425
|
}
|
|
426
|
+
function toSecondFactorColumns(secondFactor) {
|
|
427
|
+
return {
|
|
428
|
+
secondFactorMethod: secondFactor?.method ?? null,
|
|
429
|
+
secondFactorSecret: secondFactor?.secret ?? null,
|
|
430
|
+
secondFactorConfirmedAt: secondFactor?.confirmedAt ?? null,
|
|
431
|
+
secondFactorLastStep: secondFactor?.lastStep ?? null
|
|
432
|
+
};
|
|
433
|
+
}
|
|
403
434
|
function toUser(row) {
|
|
404
435
|
return {
|
|
405
436
|
id: row.id,
|
|
@@ -409,6 +440,12 @@ function toUser(row) {
|
|
|
409
440
|
fields: row.fields,
|
|
410
441
|
logins: [...row.logins],
|
|
411
442
|
password: row.passwordHash === null || row.passwordUpdatedAt === null ? null : { hash: row.passwordHash, updatedAt: row.passwordUpdatedAt },
|
|
443
|
+
secondFactor: row.secondFactorMethod === null || row.secondFactorSecret === null ? null : {
|
|
444
|
+
method: row.secondFactorMethod,
|
|
445
|
+
secret: row.secondFactorSecret,
|
|
446
|
+
confirmedAt: row.secondFactorConfirmedAt,
|
|
447
|
+
lastStep: row.secondFactorLastStep
|
|
448
|
+
},
|
|
412
449
|
emailVerifiedAt: row.emailVerifiedAt,
|
|
413
450
|
version: row.version,
|
|
414
451
|
createdAt: row.createdAt,
|
|
@@ -444,5 +481,5 @@ export {
|
|
|
444
481
|
defineJanusTables
|
|
445
482
|
};
|
|
446
483
|
|
|
447
|
-
//# debugId=
|
|
484
|
+
//# debugId=64A63C8A6A36AAEF64756E2164756E21
|
|
448
485
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sources": ["../src/relations.ts", "../src/tables.ts", "../src/translate.ts", "../src/stores.ts", "../src/adapter.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { type PgDatabase, withTransaction } from '@nxgt/drizzle/pg';\nimport {\n\ttype Entity,\n\tisSubjectSet,\n\ttype RelationTuple,\n\ttype Subject,\n\ttype SubjectSet,\n} from '@nxgt/janus';\nimport type { RelationStore } from '@nxgt/janus/permissions';\nimport { and, asc, eq, gt, isNotNull, isNull, or, type SQL } from 'drizzle-orm';\nimport {\n\ttype DrizzleAdapterOptions,\n\tdefineJanusTables,\n\ttype JanusTables,\n} from './tables';\nimport { run } from './translate';\n\n/**\n * The relation store `permissions()` takes, over one PostgreSQL database\n * through Drizzle — and the one `janus({ relations })` deletes a user's\n * tuples from.\n *\n * ```ts\n * const access = permissions({ model, store: createDrizzleRelations(db) });\n * ```\n *\n * **A write of more than one tuple is a transaction**, `@nxgt/drizzle`'s\n * `withTransaction`: removals first, then additions, all or nothing. It is not\n * retried here — a write is idempotent, so the caller retries.\n */\nexport function createDrizzleRelations(\n\tdb: PgDatabase,\n\toptions: DrizzleAdapterOptions<'relations'> = {},\n): RelationStore {\n\tconst tables = options.tables ?? defineJanusTables();\n\tconst { rowOf, subjectIs, matching } = tupleSql(tables.relations);\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('relations', operation, body);\n\n\t/** One hop from an object: its subject sets, or its entities. */\n\tconst holding = (object: Entity, relation: string, sets: boolean) =>\n\t\tdb\n\t\t\t.select({\n\t\t\t\ttype: tables.relations.subjectType,\n\t\t\t\tid: tables.relations.subjectId,\n\t\t\t\trelation: tables.relations.subjectRelation,\n\t\t\t})\n\t\t\t.from(tables.relations)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\teq(tables.relations.objectType, object.type),\n\t\t\t\t\teq(tables.relations.objectId, object.id),\n\t\t\t\t\teq(tables.relations.relation, relation),\n\t\t\t\t\tsets\n\t\t\t\t\t\t? isNotNull(tables.relations.subjectRelation)\n\t\t\t\t\t\t: isNull(tables.relations.subjectRelation),\n\t\t\t\t),\n\t\t\t);\n\n\tconst apply = async (\n\t\ttx: PgDatabase,\n\t\tadd: readonly RelationTuple[],\n\t\tremove: readonly RelationTuple[],\n\t) => {\n\t\t// In one order, so two writes touching the same tuples take their locks\n\t\t// alike and never deadlock.\n\t\tfor (const tuple of inOrder(remove)) {\n\t\t\tawait tx.delete(tables.relations).where(matching(tuple));\n\t\t}\n\t\tif (add.length > 0) {\n\t\t\t// A stored tuple meets the unique constraint, and is a no-op.\n\t\t\tawait tx\n\t\t\t\t.insert(tables.relations)\n\t\t\t\t.values(inOrder(add).map(rowOf))\n\t\t\t\t.onConflictDoNothing();\n\t\t}\n\t};\n\n\treturn {\n\t\twrite: ({ add = [], remove = [] }) =>\n\t\t\trun$('write', async () => {\n\t\t\t\tif (add.length + remove.length <= 1) return apply(db, add, remove);\n\t\t\t\tawait withTransaction(db, (tx) => apply(tx, add, remove));\n\t\t\t}),\n\n\t\thas: (tuple) =>\n\t\t\trun$('has', async () => {\n\t\t\t\tconst found = await db\n\t\t\t\t\t.select({ relation: tables.relations.relation })\n\t\t\t\t\t.from(tables.relations)\n\t\t\t\t\t.where(matching(tuple))\n\t\t\t\t\t.limit(1);\n\t\t\t\treturn found.length === 1;\n\t\t\t}),\n\n\t\tfindSubjectSets: (object, relation) =>\n\t\t\trun$('findSubjectSets', async () =>\n\t\t\t\t(await holding(object, relation, true)).map(\n\t\t\t\t\t(row): SubjectSet => ({\n\t\t\t\t\t\ttype: row.type,\n\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\trelation: row.relation ?? '',\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\n\t\tfindEntities: (object, relation) =>\n\t\t\trun$('findEntities', async () =>\n\t\t\t\t(await holding(object, relation, false)).map(\n\t\t\t\t\t(row): Entity => ({ type: row.type, id: row.id }),\n\t\t\t\t),\n\t\t\t),\n\n\t\tfindObjects: ({ type, relation, subject, after, limit }) =>\n\t\t\trun$('findObjects', async () => {\n\t\t\t\tconst conditions = [\n\t\t\t\t\tsubjectIs(subject),\n\t\t\t\t\teq(tables.relations.relation, relation),\n\t\t\t\t\teq(tables.relations.objectType, type),\n\t\t\t\t];\n\t\t\t\tif (after !== null)\n\t\t\t\t\tconditions.push(gt(tables.relations.objectId, after));\n\t\t\t\tconst found = await db\n\t\t\t\t\t.select({ id: tables.relations.objectId })\n\t\t\t\t\t.from(tables.relations)\n\t\t\t\t\t.where(and(...conditions))\n\t\t\t\t\t.orderBy(asc(tables.relations.objectId))\n\t\t\t\t\t.limit(limit + 1);\n\t\t\t\tconst items = found.slice(0, limit).map((row) => row.id);\n\t\t\t\tconst last = items.at(-1);\n\t\t\t\treturn {\n\t\t\t\t\titems,\n\t\t\t\t\tnextCursor: found.length > limit && last !== undefined ? last : null,\n\t\t\t\t};\n\t\t\t}),\n\n\t\tdeleteEntity: (entity) =>\n\t\t\trun$('deleteEntity', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.relations)\n\t\t\t\t\t.where(\n\t\t\t\t\t\tor(\n\t\t\t\t\t\t\tand(\n\t\t\t\t\t\t\t\teq(tables.relations.objectType, entity.type),\n\t\t\t\t\t\t\t\teq(tables.relations.objectId, entity.id),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tand(\n\t\t\t\t\t\t\t\teq(tables.relations.subjectType, entity.type),\n\t\t\t\t\t\t\t\teq(tables.relations.subjectId, entity.id),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t\t.returning({ relation: tables.relations.relation });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\t};\n}\n\n/** Tuples in one order: their notation, compared by code unit. */\nfunction inOrder(tuples: readonly RelationTuple[]): readonly RelationTuple[] {\n\tconst keyOf = (tuple: RelationTuple) =>\n\t\tJSON.stringify([\n\t\t\ttuple.object.type,\n\t\t\ttuple.object.id,\n\t\t\ttuple.relation,\n\t\t\ttuple.subject.type,\n\t\t\ttuple.subject.id,\n\t\t\tisSubjectSet(tuple.subject) ? tuple.subject.relation : null,\n\t\t]);\n\treturn [...tuples].sort((a, b) => {\n\t\tconst [x, y] = [keyOf(a), keyOf(b)];\n\t\treturn x < y ? -1 : x > y ? 1 : 0;\n\t});\n}\n\ntype Relations = JanusTables['relations'];\n\n/** The SQL of one tuple, over the `relations` table in use. */\nfunction tupleSql(relations: Relations) {\n\t/** A tuple as a row: an entity's `subject_relation` is `null`. */\n\tfunction rowOf(tuple: RelationTuple): Relations['$inferInsert'] {\n\t\treturn {\n\t\t\tobjectType: tuple.object.type,\n\t\t\tobjectId: tuple.object.id,\n\t\t\trelation: tuple.relation,\n\t\t\tsubjectType: tuple.subject.type,\n\t\t\tsubjectId: tuple.subject.id,\n\t\t\tsubjectRelation: isSubjectSet(tuple.subject)\n\t\t\t\t? tuple.subject.relation\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/** Exactly this subject: a subject set is not its entity, and the reverse. */\n\tfunction subjectIs(subject: Subject): SQL | undefined {\n\t\treturn and(\n\t\t\teq(relations.subjectType, subject.type),\n\t\t\teq(relations.subjectId, subject.id),\n\t\t\tisSubjectSet(subject)\n\t\t\t\t? eq(relations.subjectRelation, subject.relation)\n\t\t\t\t: isNull(relations.subjectRelation),\n\t\t);\n\t}\n\n\t/** Exactly this tuple. */\n\tfunction matching(tuple: RelationTuple): SQL | undefined {\n\t\treturn and(\n\t\t\teq(relations.objectType, tuple.object.type),\n\t\t\teq(relations.objectId, tuple.object.id),\n\t\t\teq(relations.relation, tuple.relation),\n\t\t\tsubjectIs(tuple.subject),\n\t\t);\n\t}\n\n\treturn { rowOf, subjectIs, matching };\n}\n",
|
|
6
|
-
"import { sql } from 'drizzle-orm';\nimport {\n\tboolean,\n\tcheck,\n\tcustomType,\n\tforeignKey,\n\tindex,\n\tinteger,\n\tjsonb,\n\ttype PgSchema,\n\tpgTable,\n\tprimaryKey,\n\ttext,\n\ttimestamp,\n\tunique,\n} from 'drizzle-orm/pg-core';\n\n/**\n * The five tables, as Drizzle definitions: **your migrations create them**.\n * Export them from the schema file drizzle-kit reads, and `drizzle-kit\n * generate` writes them into your next migration like any table of your own.\n *\n * **The names carry no prefix** — `users`, `logins`, `sessions`, `tokens`,\n * `relations` — because the setup this package recommends is **a database of\n * its own**, backed up and restored on its own. Beside an application's own\n * tables, give them a PostgreSQL schema instead: `{ schema: pgSchema('janus') }`\n * makes them `janus.users` and the rest, and `pg_dump -n janus` backs them up\n * alone.\n *\n * Columns are `snake_case`, as PostgreSQL's own catalog and `@nxgt/drizzle`'s\n * columns are: a camelCase column would have to be quoted in every query\n * written by hand. The records the stores answer are camelCase, as everywhere\n * in Janus.\n *\n * **What the tables hold is the port's record**, field by field. Nothing is\n * encoded, so a row read in `psql` reads like the record in the code.\n */\n\n/** Where the tables live. */\nexport interface JanusTablesOptions {\n\t/**\n\t * A PostgreSQL schema of their own, `pgSchema('janus')`, for a database the\n\t * application's tables share. **Export it from the schema file too**:\n\t * drizzle-kit writes `CREATE SCHEMA` only for a schema it finds exported.\n\t * Absent, the tables are in the connection's `search_path`, `public` by\n\t * default: the database of their own.\n\t */\n\treadonly schema?: PgSchema;\n}\n\n/**\n * A key compared byte by byte, as the port asks (rule 4): `collate \"C\"`. Under\n * a database's default collation, `en_US.UTF-8` or the like, `B` sorts after\n * `a` — and a page in \"ascending id order\" repeats or skips rows.\n */\nconst key = customType<{ data: string; driverData: string }>({\n\tdataType: () => 'text collate \"C\"',\n});\n\n// Millisecond precision: a JavaScript `Date` has no more. At PostgreSQL's\n// default of microseconds, a timestamp written and read back is not the one\n// the core wrote.\nconst at = (name: string) =>\n\ttimestamp(name, { withTimezone: true, precision: 3 });\n\n/**\n * The five tables, in the schema `options` names, or in none.\n *\n * ```ts\n * // src/janus/schema.ts — the schema file of Janus's own drizzle-kit config\n * export const { users, logins, sessions, tokens, relations } = defineJanusTables();\n *\n * // or, in a database the application's tables share\n * export const janus = pgSchema('janus');\n * export const { users, logins, sessions, tokens, relations } = defineJanusTables({ schema: janus });\n * ```\n *\n * Pass what the schema file exports to `createDrizzleAdapter` as `{ tables }`,\n * so the stores query the very tables your migration created.\n */\nexport function defineJanusTables(options: JanusTablesOptions = {}) {\n\tconst { schema } = options;\n\tconst table = (\n\t\tschema === undefined ? pgTable : schema.table\n\t) as typeof pgTable;\n\n\t/**\n\t * Users and their passwords: one row each. `logins` is the port's array,\n\t * verbatim and in order; its uniqueness is `logins`' primary key.\n\t */\n\tconst users = table(\n\t\t'users',\n\t\t{\n\t\t\t/** The UUIDv7 the core minted. `text`, not `uuid`: the store parses nothing. */\n\t\t\tid: key('id').primaryKey(),\n\t\t\ttype: key('type').notNull(),\n\t\t\tschemaVersion: text('schema_version').notNull(),\n\t\t\tactive: boolean('active').notNull(),\n\t\t\t/** The application's fields. The core validated them already. */\n\t\t\tfields: jsonb('fields').notNull(),\n\t\t\tlogins: text('logins').array().notNull(),\n\t\t\tpasswordHash: text('password_hash'),\n\t\t\tpasswordUpdatedAt: at('password_updated_at'),\n\t\t\temailVerifiedAt: at('email_verified_at'),\n\t\t\tversion: integer('version').notNull(),\n\t\t\tcreatedAt: at('created_at').notNull(),\n\t\t\tupdatedAt: at('updated_at').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\t/** What `logins` references, so a login's type is its user's. */\n\t\t\tunique('users_id_type_unique').on(t.id, t.type),\n\t\t\t/** `listUsers` reads one type in id order. */\n\t\t\tindex('users_type_id').on(t.type, t.id),\n\t\t\t/** A password is a hash and when it was set, or neither. */\n\t\t\tcheck(\n\t\t\t\t'users_password_whole',\n\t\t\t\tsql`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`,\n\t\t\t),\n\t\t],\n\t);\n\n\t/**\n\t * **The uniqueness of a login, per type** (rule 3): one row per login a\n\t * user holds, and the primary key refuses a second holder. PostgreSQL has\n\t * no unique index over the elements of an array, so this table is that\n\t * index, written in the same transaction as the user. Deleting the user\n\t * deletes its rows.\n\t */\n\tconst logins = table(\n\t\t'logins',\n\t\t{\n\t\t\ttype: key('type').notNull(),\n\t\t\tlogin: key('login').notNull(),\n\t\t\tuserId: key('user_id').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\tprimaryKey({ name: 'logins_pkey', columns: [t.type, t.login] }),\n\t\t\tforeignKey({\n\t\t\t\tname: 'logins_user_fk',\n\t\t\t\tcolumns: [t.userId, t.type],\n\t\t\t\tforeignColumns: [users.id, users.type],\n\t\t\t}).onDelete('cascade'),\n\t\t\tindex('logins_user_id').on(t.userId),\n\t\t],\n\t);\n\n\t/**\n\t * Sessions. No foreign key to the user: deleting a user deletes the user,\n\t * and the core deletes the sessions next — as it does when they live\n\t * elsewhere, in Redis.\n\t */\n\tconst sessions = table(\n\t\t'sessions',\n\t\t{\n\t\t\tid: key('id').primaryKey(),\n\t\t\t/** `sha256` of the session token. The secret itself is never stored. */\n\t\t\ttokenHash: key('token_hash').notNull(),\n\t\t\tuserId: key('user_id').notNull(),\n\t\t\tauthenticatedAt: at('authenticated_at').notNull(),\n\t\t\texpiresAt: at('expires_at').notNull(),\n\t\t\trevokedAt: at('revoked_at'),\n\t\t\tcreatedAt: at('created_at').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\tunique('sessions_token_hash_unique').on(t.tokenHash),\n\t\t\tindex('sessions_user_id').on(t.userId),\n\t\t\t/** `deleteExpiredSessions`: PostgreSQL has no TTL, so the core collects. */\n\t\t\tindex('sessions_expires_at').on(t.expiresAt),\n\t\t],\n\t);\n\n\t/** One-time tokens, keyed by their hash. */\n\tconst tokens = table(\n\t\t'tokens',\n\t\t{\n\t\t\ttokenHash: key('token_hash').primaryKey(),\n\t\t\tkind: text('kind', { enum: ['verifyEmail', 'resetPassword'] }).notNull(),\n\t\t\tuserId: key('user_id').notNull(),\n\t\t\taddress: text('address').notNull(),\n\t\t\texpiresAt: at('expires_at').notNull(),\n\t\t\tspentAt: at('spent_at'),\n\t\t\tcreatedAt: at('created_at').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\tcheck('tokens_kind', sql`${t.kind} in ('verifyEmail', 'resetPassword')`),\n\t\t\tindex('tokens_user_id').on(t.userId),\n\t\t],\n\t);\n\n\t/**\n\t * Permission tuples, one row each: `record:r1#owners@patient:u1`, or\n\t * `…@team:t1#members` for a subject set, whose `subject_relation` an\n\t * entity's row leaves `null`.\n\t *\n\t * **The uniqueness of a tuple is the unique constraint**, `nulls not\n\t * distinct` so two rows for one entity collide — PostgreSQL 15 or later.\n\t * Its column order is the reverse index `findObjects` pages: every equality\n\t * first, the object id last.\n\t */\n\tconst relations = table(\n\t\t'relations',\n\t\t{\n\t\t\tobjectType: key('object_type').notNull(),\n\t\t\tobjectId: key('object_id').notNull(),\n\t\t\trelation: key('relation').notNull(),\n\t\t\tsubjectType: key('subject_type').notNull(),\n\t\t\tsubjectId: key('subject_id').notNull(),\n\t\t\tsubjectRelation: key('subject_relation'),\n\t\t},\n\t\t(t) => [\n\t\t\tunique('relations_tuple_unique')\n\t\t\t\t.on(\n\t\t\t\t\tt.subjectType,\n\t\t\t\t\tt.subjectId,\n\t\t\t\t\tt.subjectRelation,\n\t\t\t\t\tt.relation,\n\t\t\t\t\tt.objectType,\n\t\t\t\t\tt.objectId,\n\t\t\t\t)\n\t\t\t\t.nullsNotDistinct(),\n\t\t\t/** One hop forwards: `findSubjectSets`, `findEntities`, `deleteEntity` as the object. */\n\t\t\tindex('relations_object').on(t.objectType, t.objectId, t.relation),\n\t\t],\n\t);\n\n\treturn { users, logins, sessions, tokens, relations };\n}\n\n/** The five tables `defineJanusTables` answers. */\nexport type JanusTables = ReturnType<typeof defineJanusTables>;\n\n/**\n * Which tables the stores query: **the ones your schema file exports**, so\n * the migration and the stores cannot disagree. Absent, `defineJanusTables()`:\n * the tables in the connection's `search_path`.\n *\n * ```ts\n * // src/db/schema.ts\n * export const janus = pgSchema('janus');\n * export const janusTables = defineJanusTables({ schema: janus });\n * export const { users, logins, sessions, tokens, relations } = janusTables;\n *\n * // where the adapter is wired\n * const postgres = createDrizzleAdapter(db, { tables: janusTables });\n * ```\n */\nexport interface DrizzleAdapterOptions<\n\tK extends keyof JanusTables = keyof JanusTables,\n> {\n\treadonly tables?: Pick<JanusTables, K>;\n}\n",
|
|
6
|
+
"import { sql } from 'drizzle-orm';\nimport {\n\tboolean,\n\tcheck,\n\tcustomType,\n\tforeignKey,\n\tindex,\n\tinteger,\n\tjsonb,\n\ttype PgSchema,\n\tpgTable,\n\tprimaryKey,\n\ttext,\n\ttimestamp,\n\tunique,\n} from 'drizzle-orm/pg-core';\n\n/**\n * The five tables, as Drizzle definitions: **your migrations create them**.\n * Export them from the schema file drizzle-kit reads, and `drizzle-kit\n * generate` writes them into your next migration like any table of your own.\n *\n * **The names carry no prefix** — `users`, `logins`, `sessions`, `tokens`,\n * `relations` — because the setup this package recommends is **a database of\n * its own**, backed up and restored on its own. Beside an application's own\n * tables, give them a PostgreSQL schema instead: `{ schema: pgSchema('janus') }`\n * makes them `janus.users` and the rest, and `pg_dump -n janus` backs them up\n * alone.\n *\n * Columns are `snake_case`, as PostgreSQL's own catalog and `@nxgt/drizzle`'s\n * columns are: a camelCase column would have to be quoted in every query\n * written by hand. The records the stores answer are camelCase, as everywhere\n * in Janus.\n *\n * **What the tables hold is the port's record**, field by field. Nothing is\n * encoded, so a row read in `psql` reads like the record in the code.\n */\n\n/** Where the tables live. */\nexport interface JanusTablesOptions {\n\t/**\n\t * A PostgreSQL schema of their own, `pgSchema('janus')`, for a database the\n\t * application's tables share. **Export it from the schema file too**:\n\t * drizzle-kit writes `CREATE SCHEMA` only for a schema it finds exported.\n\t * Absent, the tables are in the connection's `search_path`, `public` by\n\t * default: the database of their own.\n\t */\n\treadonly schema?: PgSchema;\n}\n\n/**\n * A key compared byte by byte, as the port asks (rule 4): `collate \"C\"`. Under\n * a database's default collation, `en_US.UTF-8` or the like, `B` sorts after\n * `a` — and a page in \"ascending id order\" repeats or skips rows.\n */\nconst key = customType<{ data: string; driverData: string }>({\n\tdataType: () => 'text collate \"C\"',\n});\n\n// Millisecond precision: a JavaScript `Date` has no more. At PostgreSQL's\n// default of microseconds, a timestamp written and read back is not the one\n// the core wrote.\nconst at = (name: string) =>\n\ttimestamp(name, { withTimezone: true, precision: 3 });\n\n/**\n * The five tables, in the schema `options` names, or in none.\n *\n * ```ts\n * // src/janus/schema.ts — the schema file of Janus's own drizzle-kit config\n * export const { users, logins, sessions, tokens, relations } = defineJanusTables();\n *\n * // or, in a database the application's tables share\n * export const janus = pgSchema('janus');\n * export const { users, logins, sessions, tokens, relations } = defineJanusTables({ schema: janus });\n * ```\n *\n * Pass what the schema file exports to `createDrizzleAdapter` as `{ tables }`,\n * so the stores query the very tables your migration created.\n */\nexport function defineJanusTables(options: JanusTablesOptions = {}) {\n\tconst { schema } = options;\n\tconst table = (\n\t\tschema === undefined ? pgTable : schema.table\n\t) as typeof pgTable;\n\n\t/**\n\t * Users and their passwords: one row each. `logins` is the port's array,\n\t * verbatim and in order; its uniqueness is `logins`' primary key.\n\t */\n\tconst users = table(\n\t\t'users',\n\t\t{\n\t\t\t/** The UUIDv7 the core minted. `text`, not `uuid`: the store parses nothing. */\n\t\t\tid: key('id').primaryKey(),\n\t\t\ttype: key('type').notNull(),\n\t\t\tschemaVersion: text('schema_version').notNull(),\n\t\t\tactive: boolean('active').notNull(),\n\t\t\t/** The application's fields. The core validated them already. */\n\t\t\tfields: jsonb('fields').notNull(),\n\t\t\tlogins: text('logins').array().notNull(),\n\t\t\tpasswordHash: text('password_hash'),\n\t\t\tpasswordUpdatedAt: at('password_updated_at'),\n\t\t\t/** `'totp'`, or `null` for a user with no second factor. */\n\t\t\tsecondFactorMethod: text('second_factor_method', { enum: ['totp'] }),\n\t\t\t/** Sealed by the core with the application's key: never the plain secret. */\n\t\t\tsecondFactorSecret: text('second_factor_secret'),\n\t\t\tsecondFactorConfirmedAt: at('second_factor_confirmed_at'),\n\t\t\tsecondFactorLastStep: integer('second_factor_last_step'),\n\t\t\temailVerifiedAt: at('email_verified_at'),\n\t\t\tversion: integer('version').notNull(),\n\t\t\tcreatedAt: at('created_at').notNull(),\n\t\t\tupdatedAt: at('updated_at').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\t/** What `logins` references, so a login's type is its user's. */\n\t\t\tunique('users_id_type_unique').on(t.id, t.type),\n\t\t\t/** `listUsers` reads one type in id order. */\n\t\t\tindex('users_type_id').on(t.type, t.id),\n\t\t\t/** A password is a hash and when it was set, or neither. */\n\t\t\tcheck(\n\t\t\t\t'users_password_whole',\n\t\t\t\tsql`(${t.passwordHash} is null) = (${t.passwordUpdatedAt} is null)`,\n\t\t\t),\n\t\t\t/**\n\t\t\t * A second factor is a method and a secret, or neither; its\n\t\t\t * confirmation and last step exist only beside them.\n\t\t\t */\n\t\t\tcheck(\n\t\t\t\t'users_second_factor_whole',\n\t\t\t\tsql`(${t.secondFactorMethod} is null) = (${t.secondFactorSecret} is null) and (${t.secondFactorMethod} is not null or (${t.secondFactorConfirmedAt} is null and ${t.secondFactorLastStep} is null))`,\n\t\t\t),\n\t\t\t/** `'totp'` is the one method; a step counts up from the epoch. */\n\t\t\tcheck(\n\t\t\t\t'users_second_factor_values',\n\t\t\t\tsql`${t.secondFactorMethod} in ('totp') and ${t.secondFactorLastStep} >= 0`,\n\t\t\t),\n\t\t],\n\t);\n\n\t/**\n\t * **The uniqueness of a login, per type** (rule 3): one row per login a\n\t * user holds, and the primary key refuses a second holder. PostgreSQL has\n\t * no unique index over the elements of an array, so this table is that\n\t * index, written in the same transaction as the user. Deleting the user\n\t * deletes its rows.\n\t */\n\tconst logins = table(\n\t\t'logins',\n\t\t{\n\t\t\ttype: key('type').notNull(),\n\t\t\tlogin: key('login').notNull(),\n\t\t\tuserId: key('user_id').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\tprimaryKey({ name: 'logins_pkey', columns: [t.type, t.login] }),\n\t\t\tforeignKey({\n\t\t\t\tname: 'logins_user_fk',\n\t\t\t\tcolumns: [t.userId, t.type],\n\t\t\t\tforeignColumns: [users.id, users.type],\n\t\t\t}).onDelete('cascade'),\n\t\t\tindex('logins_user_id').on(t.userId),\n\t\t],\n\t);\n\n\t/**\n\t * Sessions. No foreign key to the user: deleting a user deletes the user,\n\t * and the core deletes the sessions next — as it does when they live\n\t * elsewhere, in Redis.\n\t */\n\tconst sessions = table(\n\t\t'sessions',\n\t\t{\n\t\t\tid: key('id').primaryKey(),\n\t\t\t/** `sha256` of the session token. The secret itself is never stored. */\n\t\t\ttokenHash: key('token_hash').notNull(),\n\t\t\tuserId: key('user_id').notNull(),\n\t\t\tauthenticatedAt: at('authenticated_at').notNull(),\n\t\t\texpiresAt: at('expires_at').notNull(),\n\t\t\trevokedAt: at('revoked_at'),\n\t\t\tcreatedAt: at('created_at').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\tunique('sessions_token_hash_unique').on(t.tokenHash),\n\t\t\tindex('sessions_user_id').on(t.userId),\n\t\t\t/** `deleteExpiredSessions`: PostgreSQL has no TTL, so the core collects. */\n\t\t\tindex('sessions_expires_at').on(t.expiresAt),\n\t\t],\n\t);\n\n\t/** One-time tokens, keyed by their hash. */\n\tconst tokens = table(\n\t\t'tokens',\n\t\t{\n\t\t\ttokenHash: key('token_hash').primaryKey(),\n\t\t\tkind: text('kind', {\n\t\t\t\tenum: ['verifyEmail', 'resetPassword', 'secondFactor', 'signInCode'],\n\t\t\t}).notNull(),\n\t\t\tuserId: key('user_id').notNull(),\n\t\t\taddress: text('address').notNull(),\n\t\t\t/** A sign-in code's hash, keyed by the token's secret. */\n\t\t\tcodeHash: text('code_hash'),\n\t\t\t/** Codes tried against it. The default fills the rows a migration finds. */\n\t\t\tattempts: integer('attempts').notNull().default(0),\n\t\t\texpiresAt: at('expires_at').notNull(),\n\t\t\tspentAt: at('spent_at'),\n\t\t\tcreatedAt: at('created_at').notNull(),\n\t\t},\n\t\t(t) => [\n\t\t\tcheck(\n\t\t\t\t'tokens_kind',\n\t\t\t\tsql`${t.kind} in ('verifyEmail', 'resetPassword', 'secondFactor', 'signInCode')`,\n\t\t\t),\n\t\t\tcheck('tokens_attempts', sql`${t.attempts} >= 0`),\n\t\t\tindex('tokens_user_id').on(t.userId),\n\t\t],\n\t);\n\n\t/**\n\t * Permission tuples, one row each: `record:r1#owners@patient:u1`, or\n\t * `…@team:t1#members` for a subject set, whose `subject_relation` an\n\t * entity's row leaves `null`.\n\t *\n\t * **The uniqueness of a tuple is the unique constraint**, `nulls not\n\t * distinct` so two rows for one entity collide — PostgreSQL 15 or later.\n\t * Its column order is the reverse index `findObjects` pages: every equality\n\t * first, the object id last.\n\t */\n\tconst relations = table(\n\t\t'relations',\n\t\t{\n\t\t\tobjectType: key('object_type').notNull(),\n\t\t\tobjectId: key('object_id').notNull(),\n\t\t\trelation: key('relation').notNull(),\n\t\t\tsubjectType: key('subject_type').notNull(),\n\t\t\tsubjectId: key('subject_id').notNull(),\n\t\t\tsubjectRelation: key('subject_relation'),\n\t\t},\n\t\t(t) => [\n\t\t\tunique('relations_tuple_unique')\n\t\t\t\t.on(\n\t\t\t\t\tt.subjectType,\n\t\t\t\t\tt.subjectId,\n\t\t\t\t\tt.subjectRelation,\n\t\t\t\t\tt.relation,\n\t\t\t\t\tt.objectType,\n\t\t\t\t\tt.objectId,\n\t\t\t\t)\n\t\t\t\t.nullsNotDistinct(),\n\t\t\t/** One hop forwards: `findSubjectSets`, `findEntities`, `deleteEntity` as the object. */\n\t\t\tindex('relations_object').on(t.objectType, t.objectId, t.relation),\n\t\t],\n\t);\n\n\treturn { users, logins, sessions, tokens, relations };\n}\n\n/** The five tables `defineJanusTables` answers. */\nexport type JanusTables = ReturnType<typeof defineJanusTables>;\n\n/**\n * Which tables the stores query: **the ones your schema file exports**, so\n * the migration and the stores cannot disagree. Absent, `defineJanusTables()`:\n * the tables in the connection's `search_path`.\n *\n * ```ts\n * // src/db/schema.ts\n * export const janus = pgSchema('janus');\n * export const janusTables = defineJanusTables({ schema: janus });\n * export const { users, logins, sessions, tokens, relations } = janusTables;\n *\n * // where the adapter is wired\n * const postgres = createDrizzleAdapter(db, { tables: janusTables });\n * ```\n */\nexport interface DrizzleAdapterOptions<\n\tK extends keyof JanusTables = keyof JanusTables,\n> {\n\treadonly tables?: Pick<JanusTables, K>;\n}\n",
|
|
7
7
|
"import { toDataError } from '@nxgt/drizzle';\nimport { JanusError, StoreConflict, StoreFailure } from '@nxgt/janus';\n\n/**\n * **The database's errors, as the port's.** One file, and one default.\n *\n * | What happens | Becomes |\n * | --- | --- |\n * | A login another user of the type holds | `StoreConflict('login')`, carrying `login` and `userType` |\n * | A row with this id already there | nothing: the insert is a retry, and answers what is stored |\n * | A unique violation on **any other constraint** | **`StoreFailure`** |\n * | A check or foreign key refusing a row | **`StoreFailure`** |\n * | Anything else, absolutely | `StoreFailure`, with `@nxgt/drizzle`'s `DataError` as `cause` |\n *\n * The first two are never errors here: a login is written with `on conflict\n * do nothing`, and a login the statement did not write is one somebody else\n * holds — so no conflict is ever told apart by parsing a driver's message.\n *\n * The bold rows go against the obvious reading. A violation the adapter did\n * not mean to cause is an adapter bug, and reporting it as `LOGIN_TAKEN`\n * would tell somebody their login is taken when it is not. The core\n * validated every record before it reached a store, so a constraint refusing\n * one means the adapter and the port disagree — never the caller's fault.\n *\n * **The default is a failure, not an absence.** That branch is what keeps the\n * port's first rule: nothing here answers `null` for an error.\n */\n\nexport type Slot = 'users' | 'sessions' | 'tokens' | 'relations';\n\n/** Runs one port method, and makes every rejection one the port allows. */\nexport async function run<T>(\n\tslot: Slot,\n\toperation: string,\n\tbody: () => Promise<T>,\n): Promise<T> {\n\ttry {\n\t\treturn await body();\n\t} catch (error) {\n\t\t// Already the port's — a conflict, a NOT_FOUND after a read.\n\t\tif (error instanceof JanusError) throw error;\n\t\tthrow new StoreFailure(`${slot}.${operation}: the store could not answer`, {\n\t\t\tslot,\n\t\t\toperation,\n\t\t\tcause: toDataError(error),\n\t\t});\n\t}\n}\n\n/** The refusal the port promises for a taken login. */\nexport function loginTaken(\n\toperation: string,\n\ttype: string,\n\tlogin: string,\n): StoreConflict {\n\treturn new StoreConflict(\n\t\t'login',\n\t\t`${operation}: the login is taken by another ${type}`,\n\t\t{ login, userType: type, operation },\n\t);\n}\n",
|
|
8
|
-
"import { type PgDatabase, withTransaction } from '@nxgt/drizzle/pg';\nimport type {\n\tId,\n\tJanusStores,\n\tSessionRecord,\n\tSessionStore,\n\tTokenStore,\n\tUserPatch,\n\tUserRecord,\n\tUserStore,\n} from '@nxgt/janus';\nimport { NotFoundError, StoreConflict } from '@nxgt/janus';\nimport { and, asc, eq, gt, isNull, lte, ne, type SQL, sql } from 'drizzle-orm';\nimport {\n\ttype DrizzleAdapterOptions,\n\tdefineJanusTables,\n\ttype JanusTables,\n} from './tables';\nimport { loginTaken, run } from './translate';\n\ntype UserRow = JanusTables['users']['$inferSelect'];\ntype UserInsert = JanusTables['users']['$inferInsert'];\ntype SessionRow = JanusTables['sessions']['$inferSelect'];\n\n/** The four tables the identity stores query. */\ntype IdentityTable = 'users' | 'logins' | 'sessions' | 'tokens';\ntype IdentityTables = Pick<JanusTables, IdentityTable>;\n\n/**\n * The three stores `janus()` takes, over one PostgreSQL database through\n * Drizzle.\n *\n * ```ts\n * const db = drizzle(process.env.DATABASE_URL);\n * const auth = janus({ user, password: { login: 'email' }, store: createDrizzleStores(db), hasher });\n * ```\n *\n * **Every read is one statement**, whose absence is an empty result, turned\n * into `null` here; everything that arrives as a rejection is a failure. There\n * is no path on which an absence and an outage travel the same channel.\n *\n * **A user is written in one transaction** with its logins, whose table holds\n * their uniqueness — `@nxgt/drizzle`'s `withTransaction`, inside the method,\n * as rule 5 allows. Every other write is one statement.\n *\n * `sessions.deleteExpiredSessions` is implemented: PostgreSQL has no TTL, so\n * `auth.collectExpired()` is how lapsed sessions leave the table.\n *\n * `{ tables }` is what your schema file exports; absent, the tables in the\n * connection's `search_path`.\n */\nexport function createDrizzleStores(\n\tdb: PgDatabase,\n\toptions: DrizzleAdapterOptions<IdentityTable> = {},\n): JanusStores {\n\tconst tables = options.tables ?? defineJanusTables();\n\treturn {\n\t\tusers: userStore(db, tables),\n\t\tsessions: sessionStore(db, tables),\n\t\ttokens: tokenStore(db, tables),\n\t};\n}\n\nfunction userStore(db: PgDatabase, tables: IdentityTables): UserStore {\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('users', operation, body);\n\n\tconst find = async (id: string) => {\n\t\tconst [row] = await db\n\t\t\t.select()\n\t\t\t.from(tables.users)\n\t\t\t.where(eq(tables.users.id, id));\n\t\treturn row === undefined ? null : toUser(row);\n\t};\n\n\t/**\n\t * Writes the logins of one user, and throws the port's conflict for the\n\t * first one another user of the type holds. `on conflict do nothing`: a\n\t * login the statement did not write is one somebody else holds, and a\n\t * concurrent sign-up waits for the first to commit, then writes nothing.\n\t */\n\tconst claim = async (\n\t\ttx: PgDatabase,\n\t\toperation: string,\n\t\tuser: { readonly id: string; readonly type: string },\n\t\tlogins: readonly string[],\n\t) => {\n\t\t// Sorted: two transactions claiming the same logins take their locks in\n\t\t// one order, and never deadlock — the loser waits, then finds them taken.\n\t\tconst distinct = [...new Set(logins)].sort();\n\t\tif (distinct.length === 0) return;\n\t\tconst written = await tx\n\t\t\t.insert(tables.logins)\n\t\t\t.values(\n\t\t\t\tdistinct.map((login) => ({ type: user.type, login, userId: user.id })),\n\t\t\t)\n\t\t\t.onConflictDoNothing()\n\t\t\t.returning({ login: tables.logins.login });\n\t\tif (written.length === distinct.length) return;\n\t\tconst claimed = new Set(written.map((row) => row.login));\n\t\tconst taken = distinct.find((login) => !claimed.has(login)) ?? '';\n\t\tthrow loginTaken(operation, user.type, taken);\n\t};\n\n\treturn {\n\t\tinsertUser: (record) =>\n\t\t\trun$('insertUser', async () => {\n\t\t\t\tconst inserted = await withTransaction(db, async (tx) => {\n\t\t\t\t\tconst [row] = await tx\n\t\t\t\t\t\t.insert(tables.users)\n\t\t\t\t\t\t.values(toUserRow(record))\n\t\t\t\t\t\t.onConflictDoNothing({ target: tables.users.id })\n\t\t\t\t\t\t.returning();\n\t\t\t\t\tif (row === undefined) return null;\n\t\t\t\t\tawait claim(tx, 'insertUser', record, record.logins);\n\t\t\t\t\treturn toUser(row);\n\t\t\t\t});\n\t\t\t\tif (inserted !== null) return inserted;\n\n\t\t\t\t// A retry whose first attempt landed: the user with this id is\n\t\t\t\t// there, and is the answer — logins included, which are theirs.\n\t\t\t\tconst stored = await find(record.id);\n\t\t\t\tif (stored !== null) return stored;\n\t\t\t\t// Deleted between the two statements: a retry of nothing.\n\t\t\t\tthrow new NotFoundError('insertUser: the user was deleted meanwhile', {\n\t\t\t\t\tuserId: record.id,\n\t\t\t\t\toperation: 'insertUser',\n\t\t\t\t});\n\t\t\t}),\n\n\t\tfindUser: (id) => run$('findUser', () => find(id)),\n\n\t\tfindUserByLogin: (type, login) =>\n\t\t\trun$('findUserByLogin', async () => {\n\t\t\t\tconst [row] = await db\n\t\t\t\t\t.select({ user: tables.users })\n\t\t\t\t\t.from(tables.logins)\n\t\t\t\t\t.innerJoin(tables.users, eq(tables.users.id, tables.logins.userId))\n\t\t\t\t\t.where(\n\t\t\t\t\t\tand(eq(tables.logins.type, type), eq(tables.logins.login, login)),\n\t\t\t\t\t);\n\t\t\t\treturn row === undefined ? null : toUser(row.user);\n\t\t\t}),\n\n\t\tlistUsers: ({ type, after, limit }) =>\n\t\t\trun$('listUsers', async () => {\n\t\t\t\t// One more than the page, to know whether another follows.\n\t\t\t\tconst found = await db\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(tables.users)\n\t\t\t\t\t.where(\n\t\t\t\t\t\tafter === null\n\t\t\t\t\t\t\t? eq(tables.users.type, type)\n\t\t\t\t\t\t\t: and(eq(tables.users.type, type), gt(tables.users.id, after)),\n\t\t\t\t\t)\n\t\t\t\t\t.orderBy(asc(tables.users.id))\n\t\t\t\t\t.limit(limit + 1);\n\t\t\t\tconst items = found.slice(0, limit).map(toUser);\n\t\t\t\tconst last = items.at(-1);\n\t\t\t\treturn {\n\t\t\t\t\titems,\n\t\t\t\t\tnextCursor:\n\t\t\t\t\t\tfound.length > limit && last !== undefined ? last.id : null,\n\t\t\t\t};\n\t\t\t}),\n\n\t\tupdateUser: (id, patch, ifVersion) =>\n\t\t\trun$('updateUser', () =>\n\t\t\t\twithTransaction(db, async (tx) => {\n\t\t\t\t\tconst [row] = await tx\n\t\t\t\t\t\t.update(tables.users)\n\t\t\t\t\t\t.set({\n\t\t\t\t\t\t\t...toUserSet(patch),\n\t\t\t\t\t\t\tversion: sql`${tables.users.version} + 1`,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\tand(eq(tables.users.id, id), eq(tables.users.version, ifVersion)),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.returning();\n\n\t\t\t\t\tif (row === undefined) {\n\t\t\t\t\t\t// The write matched nothing, which alone cannot tell an unknown\n\t\t\t\t\t\t// id from a moved version: read again, and say which.\n\t\t\t\t\t\tconst [stored] = await tx\n\t\t\t\t\t\t\t.select({ version: tables.users.version })\n\t\t\t\t\t\t\t.from(tables.users)\n\t\t\t\t\t\t\t.where(eq(tables.users.id, id));\n\t\t\t\t\t\tif (stored === undefined) {\n\t\t\t\t\t\t\tthrow new NotFoundError('updateUser: no user has this id', {\n\t\t\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new StoreConflict(\n\t\t\t\t\t\t\t'version',\n\t\t\t\t\t\t\t`updateUser: expected version ${ifVersion}, found ${stored.version}`,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\t\t\texpectedVersion: ifVersion,\n\t\t\t\t\t\t\t\tactualVersion: stored.version,\n\t\t\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (patch.logins !== undefined) {\n\t\t\t\t\t\tawait tx.delete(tables.logins).where(eq(tables.logins.userId, id));\n\t\t\t\t\t\tawait claim(tx, 'updateUser', row, patch.logins);\n\t\t\t\t\t}\n\t\t\t\t\treturn toUser(row);\n\t\t\t\t}),\n\t\t\t),\n\n\t\tdeleteUser: (id) =>\n\t\t\trun$('deleteUser', async () => {\n\t\t\t\t// The logins go with it: their foreign key cascades.\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.users)\n\t\t\t\t\t.where(eq(tables.users.id, id))\n\t\t\t\t\t.returning({ id: tables.users.id });\n\t\t\t\treturn deleted.length === 1;\n\t\t\t}),\n\t};\n}\n\nfunction sessionStore(db: PgDatabase, tables: IdentityTables): SessionStore {\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('sessions', operation, body);\n\n\treturn {\n\t\tinsertSession: (record) =>\n\t\t\trun$('insertSession', async () => {\n\t\t\t\t// Idempotent under retry: a session with this id is already there.\n\t\t\t\t// A collision on the token hash is not a retry, and fails.\n\t\t\t\tawait db\n\t\t\t\t\t.insert(tables.sessions)\n\t\t\t\t\t.values(record)\n\t\t\t\t\t.onConflictDoNothing({ target: tables.sessions.id });\n\t\t\t}),\n\n\t\tfindSessionByTokenHash: (tokenHash) =>\n\t\t\trun$('findSessionByTokenHash', async () => {\n\t\t\t\tconst [row] = await db\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(tables.sessions)\n\t\t\t\t\t.where(eq(tables.sessions.tokenHash, tokenHash));\n\t\t\t\treturn row === undefined ? null : toSession(row);\n\t\t\t}),\n\n\t\textendSession: (id, expiresAt) =>\n\t\t\trun$('extendSession', async () => {\n\t\t\t\t// `revoked_at is null` in the condition: an extension racing a\n\t\t\t\t// revocation matches nothing, and never brings the session back.\n\t\t\t\tconst [row] = await db\n\t\t\t\t\t.update(tables.sessions)\n\t\t\t\t\t.set({ expiresAt })\n\t\t\t\t\t.where(\n\t\t\t\t\t\tand(eq(tables.sessions.id, id), isNull(tables.sessions.revokedAt)),\n\t\t\t\t\t)\n\t\t\t\t\t.returning();\n\t\t\t\treturn row === undefined ? null : toSession(row);\n\t\t\t}),\n\n\t\trevokeSession: (id, at) =>\n\t\t\trun$('revokeSession', async () => {\n\t\t\t\t// `coalesce`, so a session already revoked keeps its first\n\t\t\t\t// `revokedAt` and still counts as matched.\n\t\t\t\tconst matched = await db\n\t\t\t\t\t.update(tables.sessions)\n\t\t\t\t\t.set({\n\t\t\t\t\t\trevokedAt: sql`coalesce(${tables.sessions.revokedAt}, ${stamp(at)})`,\n\t\t\t\t\t})\n\t\t\t\t\t.where(eq(tables.sessions.id, id))\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn matched.length === 1;\n\t\t\t}),\n\n\t\trevokeUserSessions: (userId, at, except) =>\n\t\t\trun$('revokeUserSessions', async () => {\n\t\t\t\tconst standing = and(\n\t\t\t\t\teq(tables.sessions.userId, userId),\n\t\t\t\t\tisNull(tables.sessions.revokedAt),\n\t\t\t\t);\n\t\t\t\tconst revoked = await db\n\t\t\t\t\t.update(tables.sessions)\n\t\t\t\t\t.set({ revokedAt: at })\n\t\t\t\t\t.where(\n\t\t\t\t\t\texcept === undefined\n\t\t\t\t\t\t\t? standing\n\t\t\t\t\t\t\t: and(standing, ne(tables.sessions.id, except)),\n\t\t\t\t\t)\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn revoked.length;\n\t\t\t}),\n\n\t\tdeleteUserSessions: (userId) =>\n\t\t\trun$('deleteUserSessions', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.sessions)\n\t\t\t\t\t.where(eq(tables.sessions.userId, userId))\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\n\t\tdeleteExpiredSessions: (before) =>\n\t\t\trun$('deleteExpiredSessions', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.sessions)\n\t\t\t\t\t.where(lte(tables.sessions.expiresAt, before))\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\t};\n}\n\nfunction tokenStore(db: PgDatabase, tables: IdentityTables): TokenStore {\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('tokens', operation, body);\n\n\treturn {\n\t\tinsertToken: (record) =>\n\t\t\trun$('insertToken', async () => {\n\t\t\t\t// The hash is the key: a collision is a retry.\n\t\t\t\tawait db.insert(tables.tokens).values(record).onConflictDoNothing();\n\t\t\t}),\n\n\t\tconsumeToken: (tokenHash, kind, at) =>\n\t\t\trun$('consumeToken', async () => {\n\t\t\t\t// **One conditional write**, answering the row as it was before\n\t\t\t\t// it. `for update` makes a concurrent call wait for this one and\n\t\t\t\t// then read the spent row, so exactly one caller ever reads\n\t\t\t\t// `spentAt: null`; `coalesce` keeps a first `spentAt`.\n\t\t\t\tconst before = db.$with('before').as(\n\t\t\t\t\tdb\n\t\t\t\t\t\t.select()\n\t\t\t\t\t\t.from(tables.tokens)\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\tand(\n\t\t\t\t\t\t\t\teq(tables.tokens.tokenHash, tokenHash),\n\t\t\t\t\t\t\t\teq(tables.tokens.kind, kind),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.for('update'),\n\t\t\t\t);\n\t\t\t\tconst [spent] = await db\n\t\t\t\t\t.with(before)\n\t\t\t\t\t.update(tables.tokens)\n\t\t\t\t\t.set({\n\t\t\t\t\t\tspentAt: sql`coalesce(${tables.tokens.spentAt}, ${stamp(at)})`,\n\t\t\t\t\t})\n\t\t\t\t\t.from(before)\n\t\t\t\t\t.where(eq(tables.tokens.tokenHash, before.tokenHash))\n\t\t\t\t\t.returning({\n\t\t\t\t\t\ttokenHash: before.tokenHash,\n\t\t\t\t\t\tkind: before.kind,\n\t\t\t\t\t\tuserId: before.userId,\n\t\t\t\t\t\taddress: before.address,\n\t\t\t\t\t\texpiresAt: before.expiresAt,\n\t\t\t\t\t\tspentAt: before.spentAt,\n\t\t\t\t\t\tcreatedAt: before.createdAt,\n\t\t\t\t\t});\n\t\t\t\treturn spent === undefined\n\t\t\t\t\t? null\n\t\t\t\t\t: { ...spent, userId: spent.userId as Id };\n\t\t\t}),\n\n\t\tdeleteUserTokens: (userId) =>\n\t\t\trun$('deleteUserTokens', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.tokens)\n\t\t\t\t\t.where(eq(tables.tokens.userId, userId))\n\t\t\t\t\t.returning({ tokenHash: tables.tokens.tokenHash });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\t};\n}\n\n// ─── Rows and records ─────────────────────────────────────────────────────\n//\n// Each record is rebuilt field by field: the port's records are exactly what\n// it declares, and a row carries columns the record names differently.\n\nfunction toUserRow(record: UserRecord): UserInsert {\n\treturn {\n\t\tid: record.id,\n\t\ttype: record.type,\n\t\tschemaVersion: record.schemaVersion,\n\t\tactive: record.active,\n\t\tfields: record.fields,\n\t\tlogins: [...record.logins],\n\t\tpasswordHash: record.password?.hash ?? null,\n\t\tpasswordUpdatedAt: record.password?.updatedAt ?? null,\n\t\temailVerifiedAt: record.emailVerifiedAt,\n\t\tversion: record.version,\n\t\tcreatedAt: record.createdAt,\n\t\tupdatedAt: record.updatedAt,\n\t};\n}\n\n/**\n * The columns a patch names. A key present with `undefined` is absent, as the\n * port says — never an erasure. `password: null` is named, and removes the\n * password.\n */\nfunction toUserSet(patch: UserPatch): Partial<UserInsert> {\n\tconst set: Partial<UserInsert> = {\n\t\tupdatedAt: patch.updatedAt,\n\t};\n\tif (patch.schemaVersion !== undefined)\n\t\tset.schemaVersion = patch.schemaVersion;\n\tif (patch.active !== undefined) set.active = patch.active;\n\tif (patch.fields !== undefined) set.fields = patch.fields;\n\tif (patch.logins !== undefined) set.logins = [...patch.logins];\n\tif (patch.emailVerifiedAt !== undefined)\n\t\tset.emailVerifiedAt = patch.emailVerifiedAt;\n\tif (patch.password !== undefined) {\n\t\tset.passwordHash = patch.password?.hash ?? null;\n\t\tset.passwordUpdatedAt = patch.password?.updatedAt ?? null;\n\t}\n\treturn set;\n}\n\nfunction toUser(row: UserRow): UserRecord {\n\treturn {\n\t\tid: row.id as Id,\n\t\ttype: row.type,\n\t\tschemaVersion: row.schemaVersion,\n\t\tactive: row.active,\n\t\tfields: row.fields as UserRecord['fields'],\n\t\tlogins: [...row.logins],\n\t\tpassword:\n\t\t\trow.passwordHash === null || row.passwordUpdatedAt === null\n\t\t\t\t? null\n\t\t\t\t: { hash: row.passwordHash, updatedAt: row.passwordUpdatedAt },\n\t\temailVerifiedAt: row.emailVerifiedAt,\n\t\tversion: row.version,\n\t\tcreatedAt: row.createdAt,\n\t\tupdatedAt: row.updatedAt,\n\t};\n}\n\nfunction toSession(row: SessionRow): SessionRecord {\n\treturn {\n\t\tid: row.id,\n\t\ttokenHash: row.tokenHash,\n\t\tuserId: row.userId as Id,\n\t\tauthenticatedAt: row.authenticatedAt,\n\t\texpiresAt: row.expiresAt,\n\t\trevokedAt: row.revokedAt,\n\t\tcreatedAt: row.createdAt,\n\t};\n}\n\n/**\n * A `Date` inside a `sql` template, as every driver takes it: `timestamptz`\n * from its ISO string. Drizzle's column mapping does not reach a raw\n * template, and postgres.js refuses a `Date` object there.\n */\nfunction stamp(at: Date): SQL {\n\treturn sql`${at.toISOString()}::timestamptz`;\n}\n",
|
|
8
|
+
"import { type PgDatabase, withTransaction } from '@nxgt/drizzle/pg';\nimport type {\n\tId,\n\tJanusStores,\n\tSessionRecord,\n\tSessionStore,\n\tTokenStore,\n\tUserPatch,\n\tUserRecord,\n\tUserStore,\n} from '@nxgt/janus';\nimport { NotFoundError, StoreConflict } from '@nxgt/janus';\nimport { and, asc, eq, gt, isNull, lte, ne, type SQL, sql } from 'drizzle-orm';\nimport {\n\ttype DrizzleAdapterOptions,\n\tdefineJanusTables,\n\ttype JanusTables,\n} from './tables';\nimport { loginTaken, run } from './translate';\n\ntype UserRow = JanusTables['users']['$inferSelect'];\ntype UserInsert = JanusTables['users']['$inferInsert'];\ntype SessionRow = JanusTables['sessions']['$inferSelect'];\n\n/** The four tables the identity stores query. */\ntype IdentityTable = 'users' | 'logins' | 'sessions' | 'tokens';\ntype IdentityTables = Pick<JanusTables, IdentityTable>;\n\n/**\n * The three stores `janus()` takes, over one PostgreSQL database through\n * Drizzle.\n *\n * ```ts\n * const db = drizzle(process.env.DATABASE_URL);\n * const auth = janus({ user, password: { login: 'email' }, store: createDrizzleStores(db), hasher });\n * ```\n *\n * **Every read is one statement**, whose absence is an empty result, turned\n * into `null` here; everything that arrives as a rejection is a failure. There\n * is no path on which an absence and an outage travel the same channel.\n *\n * **A user is written in one transaction** with its logins, whose table holds\n * their uniqueness — `@nxgt/drizzle`'s `withTransaction`, inside the method,\n * as rule 5 allows. Every other write is one statement.\n *\n * `sessions.deleteExpiredSessions` is implemented: PostgreSQL has no TTL, so\n * `auth.collectExpired()` is how lapsed sessions leave the table.\n *\n * `{ tables }` is what your schema file exports; absent, the tables in the\n * connection's `search_path`.\n */\nexport function createDrizzleStores(\n\tdb: PgDatabase,\n\toptions: DrizzleAdapterOptions<IdentityTable> = {},\n): JanusStores {\n\tconst tables = options.tables ?? defineJanusTables();\n\treturn {\n\t\tusers: userStore(db, tables),\n\t\tsessions: sessionStore(db, tables),\n\t\ttokens: tokenStore(db, tables),\n\t};\n}\n\nfunction userStore(db: PgDatabase, tables: IdentityTables): UserStore {\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('users', operation, body);\n\n\tconst find = async (id: string) => {\n\t\tconst [row] = await db\n\t\t\t.select()\n\t\t\t.from(tables.users)\n\t\t\t.where(eq(tables.users.id, id));\n\t\treturn row === undefined ? null : toUser(row);\n\t};\n\n\t/**\n\t * Writes the logins of one user, and throws the port's conflict for the\n\t * first one another user of the type holds. `on conflict do nothing`: a\n\t * login the statement did not write is one somebody else holds, and a\n\t * concurrent sign-up waits for the first to commit, then writes nothing.\n\t */\n\tconst claim = async (\n\t\ttx: PgDatabase,\n\t\toperation: string,\n\t\tuser: { readonly id: string; readonly type: string },\n\t\tlogins: readonly string[],\n\t) => {\n\t\t// Sorted: two transactions claiming the same logins take their locks in\n\t\t// one order, and never deadlock — the loser waits, then finds them taken.\n\t\tconst distinct = [...new Set(logins)].sort();\n\t\tif (distinct.length === 0) return;\n\t\tconst written = await tx\n\t\t\t.insert(tables.logins)\n\t\t\t.values(\n\t\t\t\tdistinct.map((login) => ({ type: user.type, login, userId: user.id })),\n\t\t\t)\n\t\t\t.onConflictDoNothing()\n\t\t\t.returning({ login: tables.logins.login });\n\t\tif (written.length === distinct.length) return;\n\t\tconst claimed = new Set(written.map((row) => row.login));\n\t\tconst taken = distinct.find((login) => !claimed.has(login)) ?? '';\n\t\tthrow loginTaken(operation, user.type, taken);\n\t};\n\n\treturn {\n\t\tinsertUser: (record) =>\n\t\t\trun$('insertUser', async () => {\n\t\t\t\tconst inserted = await withTransaction(db, async (tx) => {\n\t\t\t\t\tconst [row] = await tx\n\t\t\t\t\t\t.insert(tables.users)\n\t\t\t\t\t\t.values(toUserRow(record))\n\t\t\t\t\t\t.onConflictDoNothing({ target: tables.users.id })\n\t\t\t\t\t\t.returning();\n\t\t\t\t\tif (row === undefined) return null;\n\t\t\t\t\tawait claim(tx, 'insertUser', record, record.logins);\n\t\t\t\t\treturn toUser(row);\n\t\t\t\t});\n\t\t\t\tif (inserted !== null) return inserted;\n\n\t\t\t\t// A retry whose first attempt landed: the user with this id is\n\t\t\t\t// there, and is the answer — logins included, which are theirs.\n\t\t\t\tconst stored = await find(record.id);\n\t\t\t\tif (stored !== null) return stored;\n\t\t\t\t// Deleted between the two statements: a retry of nothing.\n\t\t\t\tthrow new NotFoundError('insertUser: the user was deleted meanwhile', {\n\t\t\t\t\tuserId: record.id,\n\t\t\t\t\toperation: 'insertUser',\n\t\t\t\t});\n\t\t\t}),\n\n\t\tfindUser: (id) => run$('findUser', () => find(id)),\n\n\t\tfindUserByLogin: (type, login) =>\n\t\t\trun$('findUserByLogin', async () => {\n\t\t\t\tconst [row] = await db\n\t\t\t\t\t.select({ user: tables.users })\n\t\t\t\t\t.from(tables.logins)\n\t\t\t\t\t.innerJoin(tables.users, eq(tables.users.id, tables.logins.userId))\n\t\t\t\t\t.where(\n\t\t\t\t\t\tand(eq(tables.logins.type, type), eq(tables.logins.login, login)),\n\t\t\t\t\t);\n\t\t\t\treturn row === undefined ? null : toUser(row.user);\n\t\t\t}),\n\n\t\tlistUsers: ({ type, after, limit }) =>\n\t\t\trun$('listUsers', async () => {\n\t\t\t\t// One more than the page, to know whether another follows.\n\t\t\t\tconst found = await db\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(tables.users)\n\t\t\t\t\t.where(\n\t\t\t\t\t\tafter === null\n\t\t\t\t\t\t\t? eq(tables.users.type, type)\n\t\t\t\t\t\t\t: and(eq(tables.users.type, type), gt(tables.users.id, after)),\n\t\t\t\t\t)\n\t\t\t\t\t.orderBy(asc(tables.users.id))\n\t\t\t\t\t.limit(limit + 1);\n\t\t\t\tconst items = found.slice(0, limit).map(toUser);\n\t\t\t\tconst last = items.at(-1);\n\t\t\t\treturn {\n\t\t\t\t\titems,\n\t\t\t\t\tnextCursor:\n\t\t\t\t\t\tfound.length > limit && last !== undefined ? last.id : null,\n\t\t\t\t};\n\t\t\t}),\n\n\t\tupdateUser: (id, patch, ifVersion) =>\n\t\t\trun$('updateUser', () =>\n\t\t\t\twithTransaction(db, async (tx) => {\n\t\t\t\t\tconst [row] = await tx\n\t\t\t\t\t\t.update(tables.users)\n\t\t\t\t\t\t.set({\n\t\t\t\t\t\t\t...toUserSet(patch),\n\t\t\t\t\t\t\tversion: sql`${tables.users.version} + 1`,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\tand(eq(tables.users.id, id), eq(tables.users.version, ifVersion)),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.returning();\n\n\t\t\t\t\tif (row === undefined) {\n\t\t\t\t\t\t// The write matched nothing, which alone cannot tell an unknown\n\t\t\t\t\t\t// id from a moved version: read again, and say which.\n\t\t\t\t\t\tconst [stored] = await tx\n\t\t\t\t\t\t\t.select({ version: tables.users.version })\n\t\t\t\t\t\t\t.from(tables.users)\n\t\t\t\t\t\t\t.where(eq(tables.users.id, id));\n\t\t\t\t\t\tif (stored === undefined) {\n\t\t\t\t\t\t\tthrow new NotFoundError('updateUser: no user has this id', {\n\t\t\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new StoreConflict(\n\t\t\t\t\t\t\t'version',\n\t\t\t\t\t\t\t`updateUser: expected version ${ifVersion}, found ${stored.version}`,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tuserId: id,\n\t\t\t\t\t\t\t\texpectedVersion: ifVersion,\n\t\t\t\t\t\t\t\tactualVersion: stored.version,\n\t\t\t\t\t\t\t\toperation: 'updateUser',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (patch.logins !== undefined) {\n\t\t\t\t\t\tawait tx.delete(tables.logins).where(eq(tables.logins.userId, id));\n\t\t\t\t\t\tawait claim(tx, 'updateUser', row, patch.logins);\n\t\t\t\t\t}\n\t\t\t\t\treturn toUser(row);\n\t\t\t\t}),\n\t\t\t),\n\n\t\tdeleteUser: (id) =>\n\t\t\trun$('deleteUser', async () => {\n\t\t\t\t// The logins go with it: their foreign key cascades.\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.users)\n\t\t\t\t\t.where(eq(tables.users.id, id))\n\t\t\t\t\t.returning({ id: tables.users.id });\n\t\t\t\treturn deleted.length === 1;\n\t\t\t}),\n\t};\n}\n\nfunction sessionStore(db: PgDatabase, tables: IdentityTables): SessionStore {\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('sessions', operation, body);\n\n\treturn {\n\t\tinsertSession: (record) =>\n\t\t\trun$('insertSession', async () => {\n\t\t\t\t// Idempotent under retry: a session with this id is already there.\n\t\t\t\t// A collision on the token hash is not a retry, and fails.\n\t\t\t\tawait db\n\t\t\t\t\t.insert(tables.sessions)\n\t\t\t\t\t.values(record)\n\t\t\t\t\t.onConflictDoNothing({ target: tables.sessions.id });\n\t\t\t}),\n\n\t\tfindSessionByTokenHash: (tokenHash) =>\n\t\t\trun$('findSessionByTokenHash', async () => {\n\t\t\t\tconst [row] = await db\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(tables.sessions)\n\t\t\t\t\t.where(eq(tables.sessions.tokenHash, tokenHash));\n\t\t\t\treturn row === undefined ? null : toSession(row);\n\t\t\t}),\n\n\t\textendSession: (id, expiresAt) =>\n\t\t\trun$('extendSession', async () => {\n\t\t\t\t// `revoked_at is null` in the condition: an extension racing a\n\t\t\t\t// revocation matches nothing, and never brings the session back.\n\t\t\t\tconst [row] = await db\n\t\t\t\t\t.update(tables.sessions)\n\t\t\t\t\t.set({ expiresAt })\n\t\t\t\t\t.where(\n\t\t\t\t\t\tand(eq(tables.sessions.id, id), isNull(tables.sessions.revokedAt)),\n\t\t\t\t\t)\n\t\t\t\t\t.returning();\n\t\t\t\treturn row === undefined ? null : toSession(row);\n\t\t\t}),\n\n\t\trevokeSession: (id, at) =>\n\t\t\trun$('revokeSession', async () => {\n\t\t\t\t// `coalesce`, so a session already revoked keeps its first\n\t\t\t\t// `revokedAt` and still counts as matched.\n\t\t\t\tconst matched = await db\n\t\t\t\t\t.update(tables.sessions)\n\t\t\t\t\t.set({\n\t\t\t\t\t\trevokedAt: sql`coalesce(${tables.sessions.revokedAt}, ${stamp(at)})`,\n\t\t\t\t\t})\n\t\t\t\t\t.where(eq(tables.sessions.id, id))\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn matched.length === 1;\n\t\t\t}),\n\n\t\trevokeUserSessions: (userId, at, except) =>\n\t\t\trun$('revokeUserSessions', async () => {\n\t\t\t\tconst standing = and(\n\t\t\t\t\teq(tables.sessions.userId, userId),\n\t\t\t\t\tisNull(tables.sessions.revokedAt),\n\t\t\t\t);\n\t\t\t\tconst revoked = await db\n\t\t\t\t\t.update(tables.sessions)\n\t\t\t\t\t.set({ revokedAt: at })\n\t\t\t\t\t.where(\n\t\t\t\t\t\texcept === undefined\n\t\t\t\t\t\t\t? standing\n\t\t\t\t\t\t\t: and(standing, ne(tables.sessions.id, except)),\n\t\t\t\t\t)\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn revoked.length;\n\t\t\t}),\n\n\t\tdeleteUserSessions: (userId) =>\n\t\t\trun$('deleteUserSessions', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.sessions)\n\t\t\t\t\t.where(eq(tables.sessions.userId, userId))\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\n\t\tdeleteExpiredSessions: (before) =>\n\t\t\trun$('deleteExpiredSessions', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.sessions)\n\t\t\t\t\t.where(lte(tables.sessions.expiresAt, before))\n\t\t\t\t\t.returning({ id: tables.sessions.id });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\t};\n}\n\nfunction tokenStore(db: PgDatabase, tables: IdentityTables): TokenStore {\n\tconst run$ = <T>(operation: string, body: () => Promise<T>) =>\n\t\trun('tokens', operation, body);\n\n\treturn {\n\t\tinsertToken: (record) =>\n\t\t\trun$('insertToken', async () => {\n\t\t\t\t// The hash is the key: a collision is a retry.\n\t\t\t\tawait db.insert(tables.tokens).values(record).onConflictDoNothing();\n\t\t\t}),\n\n\t\tconsumeToken: (tokenHash, kind, at) =>\n\t\t\trun$('consumeToken', async () => {\n\t\t\t\t// **One conditional write**, answering the row as it was before\n\t\t\t\t// it. `for update` makes a concurrent call wait for this one and\n\t\t\t\t// then read the spent row, so exactly one caller ever reads\n\t\t\t\t// `spentAt: null`; `coalesce` keeps a first `spentAt`.\n\t\t\t\tconst before = db.$with('before').as(\n\t\t\t\t\tdb\n\t\t\t\t\t\t.select()\n\t\t\t\t\t\t.from(tables.tokens)\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\tand(\n\t\t\t\t\t\t\t\teq(tables.tokens.tokenHash, tokenHash),\n\t\t\t\t\t\t\t\teq(tables.tokens.kind, kind),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.for('update'),\n\t\t\t\t);\n\t\t\t\tconst [spent] = await db\n\t\t\t\t\t.with(before)\n\t\t\t\t\t.update(tables.tokens)\n\t\t\t\t\t.set({\n\t\t\t\t\t\tspentAt: sql`coalesce(${tables.tokens.spentAt}, ${stamp(at)})`,\n\t\t\t\t\t})\n\t\t\t\t\t.from(before)\n\t\t\t\t\t.where(eq(tables.tokens.tokenHash, before.tokenHash))\n\t\t\t\t\t.returning({\n\t\t\t\t\t\ttokenHash: before.tokenHash,\n\t\t\t\t\t\tkind: before.kind,\n\t\t\t\t\t\tuserId: before.userId,\n\t\t\t\t\t\taddress: before.address,\n\t\t\t\t\t\tcodeHash: before.codeHash,\n\t\t\t\t\t\tattempts: before.attempts,\n\t\t\t\t\t\texpiresAt: before.expiresAt,\n\t\t\t\t\t\tspentAt: before.spentAt,\n\t\t\t\t\t\tcreatedAt: before.createdAt,\n\t\t\t\t\t});\n\t\t\t\treturn spent === undefined\n\t\t\t\t\t? null\n\t\t\t\t\t: { ...spent, userId: spent.userId as Id };\n\t\t\t}),\n\n\t\tcountAttempt: (tokenHash, kind) =>\n\t\t\trun$('countAttempt', async () => {\n\t\t\t\t// **One conditional write**: `attempts + 1` on the row, under the\n\t\t\t\t// row's lock, so twenty concurrent calls answer twenty counts. A\n\t\t\t\t// spent token matches nothing here, and is read as it is below.\n\t\t\t\tconst match = and(\n\t\t\t\t\teq(tables.tokens.tokenHash, tokenHash),\n\t\t\t\t\teq(tables.tokens.kind, kind),\n\t\t\t\t);\n\t\t\t\tconst [counted] = await db\n\t\t\t\t\t.update(tables.tokens)\n\t\t\t\t\t.set({ attempts: sql`${tables.tokens.attempts} + 1` })\n\t\t\t\t\t.where(and(match, isNull(tables.tokens.spentAt)))\n\t\t\t\t\t.returning();\n\t\t\t\tconst row =\n\t\t\t\t\tcounted ?? (await db.select().from(tables.tokens).where(match))[0];\n\t\t\t\treturn row === undefined ? null : { ...row, userId: row.userId as Id };\n\t\t\t}),\n\n\t\tdeleteUserTokens: (userId) =>\n\t\t\trun$('deleteUserTokens', async () => {\n\t\t\t\tconst deleted = await db\n\t\t\t\t\t.delete(tables.tokens)\n\t\t\t\t\t.where(eq(tables.tokens.userId, userId))\n\t\t\t\t\t.returning({ tokenHash: tables.tokens.tokenHash });\n\t\t\t\treturn deleted.length;\n\t\t\t}),\n\t};\n}\n\n// ─── Rows and records ─────────────────────────────────────────────────────\n//\n// Each record is rebuilt field by field: the port's records are exactly what\n// it declares, and a row carries columns the record names differently.\n\nfunction toUserRow(record: UserRecord): UserInsert {\n\treturn {\n\t\tid: record.id,\n\t\ttype: record.type,\n\t\tschemaVersion: record.schemaVersion,\n\t\tactive: record.active,\n\t\tfields: record.fields,\n\t\tlogins: [...record.logins],\n\t\tpasswordHash: record.password?.hash ?? null,\n\t\tpasswordUpdatedAt: record.password?.updatedAt ?? null,\n\t\t...toSecondFactorColumns(record.secondFactor),\n\t\temailVerifiedAt: record.emailVerifiedAt,\n\t\tversion: record.version,\n\t\tcreatedAt: record.createdAt,\n\t\tupdatedAt: record.updatedAt,\n\t};\n}\n\n/**\n * The columns a patch names. A key present with `undefined` is absent, as the\n * port says — never an erasure. `password: null` is named, and removes the\n * password.\n */\nfunction toUserSet(patch: UserPatch): Partial<UserInsert> {\n\tconst set: Partial<UserInsert> = {\n\t\tupdatedAt: patch.updatedAt,\n\t};\n\tif (patch.schemaVersion !== undefined)\n\t\tset.schemaVersion = patch.schemaVersion;\n\tif (patch.active !== undefined) set.active = patch.active;\n\tif (patch.fields !== undefined) set.fields = patch.fields;\n\tif (patch.logins !== undefined) set.logins = [...patch.logins];\n\tif (patch.emailVerifiedAt !== undefined)\n\t\tset.emailVerifiedAt = patch.emailVerifiedAt;\n\tif (patch.password !== undefined) {\n\t\tset.passwordHash = patch.password?.hash ?? null;\n\t\tset.passwordUpdatedAt = patch.password?.updatedAt ?? null;\n\t}\n\tif (patch.secondFactor !== undefined) {\n\t\tObject.assign(set, toSecondFactorColumns(patch.secondFactor));\n\t}\n\treturn set;\n}\n\n/** A second factor as its four columns, all `null` for none. */\nfunction toSecondFactorColumns(secondFactor: UserRecord['secondFactor']) {\n\treturn {\n\t\tsecondFactorMethod: secondFactor?.method ?? null,\n\t\tsecondFactorSecret: secondFactor?.secret ?? null,\n\t\tsecondFactorConfirmedAt: secondFactor?.confirmedAt ?? null,\n\t\tsecondFactorLastStep: secondFactor?.lastStep ?? null,\n\t};\n}\n\nfunction toUser(row: UserRow): UserRecord {\n\treturn {\n\t\tid: row.id as Id,\n\t\ttype: row.type,\n\t\tschemaVersion: row.schemaVersion,\n\t\tactive: row.active,\n\t\tfields: row.fields as UserRecord['fields'],\n\t\tlogins: [...row.logins],\n\t\tpassword:\n\t\t\trow.passwordHash === null || row.passwordUpdatedAt === null\n\t\t\t\t? null\n\t\t\t\t: { hash: row.passwordHash, updatedAt: row.passwordUpdatedAt },\n\t\tsecondFactor:\n\t\t\trow.secondFactorMethod === null || row.secondFactorSecret === null\n\t\t\t\t? null\n\t\t\t\t: {\n\t\t\t\t\t\tmethod: row.secondFactorMethod,\n\t\t\t\t\t\tsecret: row.secondFactorSecret,\n\t\t\t\t\t\tconfirmedAt: row.secondFactorConfirmedAt,\n\t\t\t\t\t\tlastStep: row.secondFactorLastStep,\n\t\t\t\t\t},\n\t\temailVerifiedAt: row.emailVerifiedAt,\n\t\tversion: row.version,\n\t\tcreatedAt: row.createdAt,\n\t\tupdatedAt: row.updatedAt,\n\t};\n}\n\nfunction toSession(row: SessionRow): SessionRecord {\n\treturn {\n\t\tid: row.id,\n\t\ttokenHash: row.tokenHash,\n\t\tuserId: row.userId as Id,\n\t\tauthenticatedAt: row.authenticatedAt,\n\t\texpiresAt: row.expiresAt,\n\t\trevokedAt: row.revokedAt,\n\t\tcreatedAt: row.createdAt,\n\t};\n}\n\n/**\n * A `Date` inside a `sql` template, as every driver takes it: `timestamptz`\n * from its ISO string. Drizzle's column mapping does not reach a raw\n * template, and postgres.js refuses a `Date` object there.\n */\nfunction stamp(at: Date): SQL {\n\treturn sql`${at.toISOString()}::timestamptz`;\n}\n",
|
|
9
9
|
"import type { PgDatabase } from '@nxgt/drizzle/pg';\nimport type { JanusStores } from '@nxgt/janus';\nimport type { RelationStore } from '@nxgt/janus/permissions';\nimport { createDrizzleRelations } from './relations';\nimport { createDrizzleStores } from './stores';\nimport type { DrizzleAdapterOptions } from './tables';\n\n/** The whole adapter over one database, keyed as `janus()` takes it. */\nexport interface DrizzleAdapter {\n\t/** The identity stores: `createDrizzleStores(db)`. */\n\treadonly store: JanusStores;\n\t/** The relation store: `createDrizzleRelations(db)`. */\n\treadonly relations: RelationStore;\n}\n\n/**\n * The identity stores and the relation store over one database, under the\n * names `janus()` takes them — so one spread wires both, and deleting a user\n * deletes every tuple naming them:\n *\n * ```ts\n * const postgres = createDrizzleAdapter(db);\n * const auth = janus({ user, password: { login: 'email' }, hasher, ...postgres });\n * const access = permissions({ model, store: postgres.relations });\n * ```\n *\n * It connects to nothing and creates nothing: `db` is your Drizzle instance,\n * and the tables are your migrations'. `{ tables }` is what your schema file\n * exports, needed when they are in a PostgreSQL schema of their own. An\n * application that only authenticates can take `createDrizzleStores(db)` alone.\n */\nexport function createDrizzleAdapter(\n\tdb: PgDatabase,\n\toptions: DrizzleAdapterOptions = {},\n): DrizzleAdapter {\n\treturn {\n\t\tstore: createDrizzleStores(db, options),\n\t\trelations: createDrizzleRelations(db, options),\n\t};\n}\n"
|
|
10
10
|
],
|
|
11
|
-
"mappings": "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",
|
|
12
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "64A63C8A6A36AAEF64756E2164756E21",
|
|
13
13
|
"names": []
|
|
14
14
|
}
|
package/dist/tables.d.ts
CHANGED
|
@@ -178,6 +178,66 @@ export declare function defineJanusTables(options?: JanusTablesOptions): {
|
|
|
178
178
|
identity: undefined;
|
|
179
179
|
generated: undefined;
|
|
180
180
|
}>;
|
|
181
|
+
secondFactorMethod: import("drizzle-orm/pg-core").PgBuildColumn<"users", import("drizzle-orm/pg-core").PgTextBuilder<["totp"]>, {
|
|
182
|
+
name: string;
|
|
183
|
+
tableName: "users";
|
|
184
|
+
dataType: "string enum";
|
|
185
|
+
data: "totp";
|
|
186
|
+
driverParam: string;
|
|
187
|
+
notNull: false;
|
|
188
|
+
hasDefault: false;
|
|
189
|
+
isPrimaryKey: false;
|
|
190
|
+
isAutoincrement: false;
|
|
191
|
+
hasRuntimeDefault: false;
|
|
192
|
+
enumValues: ["totp"];
|
|
193
|
+
identity: undefined;
|
|
194
|
+
generated: undefined;
|
|
195
|
+
}>;
|
|
196
|
+
secondFactorSecret: import("drizzle-orm/pg-core").PgBuildColumn<"users", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
|
|
197
|
+
name: string;
|
|
198
|
+
tableName: "users";
|
|
199
|
+
dataType: "string";
|
|
200
|
+
data: string;
|
|
201
|
+
driverParam: string;
|
|
202
|
+
notNull: false;
|
|
203
|
+
hasDefault: false;
|
|
204
|
+
isPrimaryKey: false;
|
|
205
|
+
isAutoincrement: false;
|
|
206
|
+
hasRuntimeDefault: false;
|
|
207
|
+
enumValues: undefined;
|
|
208
|
+
identity: undefined;
|
|
209
|
+
generated: undefined;
|
|
210
|
+
}>;
|
|
211
|
+
secondFactorConfirmedAt: import("drizzle-orm/pg-core").PgBuildColumn<"users", import("drizzle-orm/pg-core").PgTimestampBuilder, {
|
|
212
|
+
name: string;
|
|
213
|
+
tableName: "users";
|
|
214
|
+
dataType: "object date";
|
|
215
|
+
data: Date;
|
|
216
|
+
driverParam: string;
|
|
217
|
+
notNull: false;
|
|
218
|
+
hasDefault: false;
|
|
219
|
+
isPrimaryKey: false;
|
|
220
|
+
isAutoincrement: false;
|
|
221
|
+
hasRuntimeDefault: false;
|
|
222
|
+
enumValues: undefined;
|
|
223
|
+
identity: undefined;
|
|
224
|
+
generated: undefined;
|
|
225
|
+
}>;
|
|
226
|
+
secondFactorLastStep: import("drizzle-orm/pg-core").PgBuildColumn<"users", import("drizzle-orm/pg-core").PgIntegerBuilder, {
|
|
227
|
+
name: string;
|
|
228
|
+
tableName: "users";
|
|
229
|
+
dataType: "number int32";
|
|
230
|
+
data: number;
|
|
231
|
+
driverParam: string | number;
|
|
232
|
+
notNull: false;
|
|
233
|
+
hasDefault: false;
|
|
234
|
+
isPrimaryKey: false;
|
|
235
|
+
isAutoincrement: false;
|
|
236
|
+
hasRuntimeDefault: false;
|
|
237
|
+
enumValues: undefined;
|
|
238
|
+
identity: undefined;
|
|
239
|
+
generated: undefined;
|
|
240
|
+
}>;
|
|
181
241
|
emailVerifiedAt: import("drizzle-orm/pg-core").PgBuildColumn<"users", import("drizzle-orm/pg-core").PgTimestampBuilder, {
|
|
182
242
|
name: string;
|
|
183
243
|
tableName: "users";
|
|
@@ -452,18 +512,18 @@ export declare function defineJanusTables(options?: JanusTablesOptions): {
|
|
|
452
512
|
identity: undefined;
|
|
453
513
|
generated: undefined;
|
|
454
514
|
}>;
|
|
455
|
-
kind: import("drizzle-orm/pg-core").PgBuildColumn<"tokens", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<["verifyEmail", "resetPassword"]>>, {
|
|
515
|
+
kind: import("drizzle-orm/pg-core").PgBuildColumn<"tokens", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<["verifyEmail", "resetPassword", "secondFactor", "signInCode"]>>, {
|
|
456
516
|
name: string;
|
|
457
517
|
tableName: "tokens";
|
|
458
518
|
dataType: "string enum";
|
|
459
|
-
data: "verifyEmail" | "resetPassword";
|
|
519
|
+
data: "verifyEmail" | "resetPassword" | "secondFactor" | "signInCode";
|
|
460
520
|
driverParam: string;
|
|
461
521
|
notNull: true;
|
|
462
522
|
hasDefault: false;
|
|
463
523
|
isPrimaryKey: false;
|
|
464
524
|
isAutoincrement: false;
|
|
465
525
|
hasRuntimeDefault: false;
|
|
466
|
-
enumValues: ["verifyEmail", "resetPassword"];
|
|
526
|
+
enumValues: ["verifyEmail", "resetPassword", "secondFactor", "signInCode"];
|
|
467
527
|
identity: undefined;
|
|
468
528
|
generated: undefined;
|
|
469
529
|
}>;
|
|
@@ -501,6 +561,36 @@ export declare function defineJanusTables(options?: JanusTablesOptions): {
|
|
|
501
561
|
identity: undefined;
|
|
502
562
|
generated: undefined;
|
|
503
563
|
}>;
|
|
564
|
+
codeHash: import("drizzle-orm/pg-core").PgBuildColumn<"tokens", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
|
|
565
|
+
name: string;
|
|
566
|
+
tableName: "tokens";
|
|
567
|
+
dataType: "string";
|
|
568
|
+
data: string;
|
|
569
|
+
driverParam: string;
|
|
570
|
+
notNull: false;
|
|
571
|
+
hasDefault: false;
|
|
572
|
+
isPrimaryKey: false;
|
|
573
|
+
isAutoincrement: false;
|
|
574
|
+
hasRuntimeDefault: false;
|
|
575
|
+
enumValues: undefined;
|
|
576
|
+
identity: undefined;
|
|
577
|
+
generated: undefined;
|
|
578
|
+
}>;
|
|
579
|
+
attempts: import("drizzle-orm/pg-core").PgBuildColumn<"tokens", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgIntegerBuilder>>, {
|
|
580
|
+
name: string;
|
|
581
|
+
tableName: "tokens";
|
|
582
|
+
dataType: "number int32";
|
|
583
|
+
data: number;
|
|
584
|
+
driverParam: string | number;
|
|
585
|
+
notNull: true;
|
|
586
|
+
hasDefault: true;
|
|
587
|
+
isPrimaryKey: false;
|
|
588
|
+
isAutoincrement: false;
|
|
589
|
+
hasRuntimeDefault: false;
|
|
590
|
+
enumValues: undefined;
|
|
591
|
+
identity: undefined;
|
|
592
|
+
generated: undefined;
|
|
593
|
+
}>;
|
|
504
594
|
expiresAt: import("drizzle-orm/pg-core").PgBuildColumn<"tokens", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>, {
|
|
505
595
|
name: string;
|
|
506
596
|
tableName: "tokens";
|
package/dist/tables.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AACA,OAAO,EAQN,KAAK,QAAQ,EAMb,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,6BAA6B;AAC7B,MAAM,WAAW,kBAAkB;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AACA,OAAO,EAQN,KAAK,QAAQ,EAMb,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,6BAA6B;AAC7B,MAAM,WAAW,kBAAkB;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+KjE;AAED,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,qBAAqB,CACrC,CAAC,SAAS,MAAM,WAAW,GAAG,MAAM,WAAW;IAE/C,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;CACvC"}
|
package/docs/README.md
CHANGED
|
@@ -7,6 +7,6 @@ give the detail.
|
|
|
7
7
|
| --- | --- |
|
|
8
8
|
| [Wiring the stores and the relations](guide/wiring.md) | You are passing your Drizzle instance to `janus()` and `permissions()`, choosing a driver, or handling what a failure throws |
|
|
9
9
|
| [Where the tables live](guide/database.md) | You are choosing between a database of Janus's own and a PostgreSQL schema in yours, or backing up and restoring Janus's tables |
|
|
10
|
-
| [The tables in your migrations](guide/migrations.md) | You are adding the tables to your schema, generating the migration, collecting lapsed sessions, or want to know what the database holds |
|
|
10
|
+
| [The tables in your migrations](guide/migrations.md) | You are adding the tables to your schema, generating the migration, upgrading to a version whose tables changed, collecting lapsed sessions, or want to know what the database holds |
|
|
11
11
|
| [Troubleshooting](troubleshooting.md) | You have an error message and want its cause and its fix |
|
|
12
12
|
| [Roadmap](roadmap.md) | You want to know what is coming, what shipped, and what is deliberately not planned |
|
package/docs/guide/migrations.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
This page covers:
|
|
4
4
|
- adding the five tables to the schema drizzle-kit reads;
|
|
5
5
|
- what the migration creates;
|
|
6
|
+
- upgrading: the migration a new version of the tables needs;
|
|
6
7
|
- collecting lapsed sessions;
|
|
7
8
|
- what the database holds.
|
|
8
9
|
|
|
@@ -64,12 +65,18 @@ CREATE TABLE "users" (
|
|
|
64
65
|
"logins" text[] NOT NULL,
|
|
65
66
|
"password_hash" text,
|
|
66
67
|
"password_updated_at" timestamp(3) with time zone,
|
|
68
|
+
"second_factor_method" text,
|
|
69
|
+
"second_factor_secret" text,
|
|
70
|
+
"second_factor_confirmed_at" timestamp(3) with time zone,
|
|
71
|
+
"second_factor_last_step" integer,
|
|
67
72
|
"email_verified_at" timestamp(3) with time zone,
|
|
68
73
|
"version" integer NOT NULL,
|
|
69
74
|
"created_at" timestamp(3) with time zone NOT NULL,
|
|
70
75
|
"updated_at" timestamp(3) with time zone NOT NULL,
|
|
71
76
|
CONSTRAINT "users_id_type_unique" UNIQUE("id","type"),
|
|
72
|
-
CONSTRAINT "users_password_whole" CHECK (("password_hash" is null) = ("password_updated_at" is null))
|
|
77
|
+
CONSTRAINT "users_password_whole" CHECK (("password_hash" is null) = ("password_updated_at" is null)),
|
|
78
|
+
CONSTRAINT "users_second_factor_whole" CHECK (("second_factor_method" is null) = ("second_factor_secret" is null) and ("second_factor_method" is not null or ("second_factor_confirmed_at" is null and "second_factor_last_step" is null))),
|
|
79
|
+
CONSTRAINT "users_second_factor_values" CHECK ("second_factor_method" in ('totp') and "second_factor_last_step" >= 0)
|
|
73
80
|
);
|
|
74
81
|
CREATE TABLE "logins" (
|
|
75
82
|
"type" text collate "C",
|
|
@@ -77,7 +84,20 @@ CREATE TABLE "logins" (
|
|
|
77
84
|
"user_id" text collate "C" NOT NULL,
|
|
78
85
|
CONSTRAINT "logins_pkey" PRIMARY KEY("type","login")
|
|
79
86
|
);
|
|
80
|
-
|
|
87
|
+
CREATE TABLE "tokens" (
|
|
88
|
+
"token_hash" text collate "C" PRIMARY KEY,
|
|
89
|
+
"kind" text NOT NULL,
|
|
90
|
+
"user_id" text collate "C" NOT NULL,
|
|
91
|
+
"address" text NOT NULL,
|
|
92
|
+
"code_hash" text,
|
|
93
|
+
"attempts" integer DEFAULT 0 NOT NULL,
|
|
94
|
+
"expires_at" timestamp(3) with time zone NOT NULL,
|
|
95
|
+
"spent_at" timestamp(3) with time zone,
|
|
96
|
+
"created_at" timestamp(3) with time zone NOT NULL,
|
|
97
|
+
CONSTRAINT "tokens_kind" CHECK ("kind" in ('verifyEmail', 'resetPassword', 'secondFactor', 'signInCode')),
|
|
98
|
+
CONSTRAINT "tokens_attempts" CHECK ("attempts" >= 0)
|
|
99
|
+
);
|
|
100
|
+
-- sessions, relations, the indexes, and the foreign key from
|
|
81
101
|
-- logins to users, on delete cascade.
|
|
82
102
|
```
|
|
83
103
|
|
|
@@ -111,10 +131,55 @@ migration creates is what the conformance suites ran on.
|
|
|
111
131
|
returned verbatim and in order, and as rows in `logins`, whose primary
|
|
112
132
|
key is the per-type uniqueness. PostgreSQL has no unique index over the
|
|
113
133
|
elements of an array.
|
|
134
|
+
- **A second factor in four columns, checked whole.** A method and a secret,
|
|
135
|
+
or neither; a confirmation date and a last step only beside them; `totp`
|
|
136
|
+
the only method, and a last step never negative. The
|
|
137
|
+
secret is opaque to the store and kept byte for byte: once the second
|
|
138
|
+
factor ships, `@nxgt/janus` will seal it before the store sees it, so a
|
|
139
|
+
dump of `users` cannot produce a code.
|
|
140
|
+
- **`attempts` is `not null default 0`**, checked `>= 0`, so the rows an upgrade finds read
|
|
141
|
+
as tokens with no attempt counted yet.
|
|
114
142
|
- **No foreign key from sessions and tokens to users.** Deleting a user
|
|
115
143
|
deletes the user; the core deletes the sessions and tokens next, as it does
|
|
116
144
|
when they live in another store.
|
|
117
145
|
|
|
146
|
+
## Upgrading
|
|
147
|
+
|
|
148
|
+
A new version of the tables is a migration, like a change to your own. Run
|
|
149
|
+
`drizzle-kit generate` after upgrading the package, read what it wrote, and
|
|
150
|
+
migrate **before** deploying the code that reads the new columns:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
bun add @nxgt/janus-drizzle@latest @nxgt/janus@latest
|
|
154
|
+
bunx drizzle-kit generate --config drizzle.janus.config.ts
|
|
155
|
+
bunx drizzle-kit migrate --config drizzle.janus.config.ts
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Deployed first, every query on `users` or `tokens` fails with `STORE_FAILED`,
|
|
159
|
+
caused by `column "…" does not exist`.
|
|
160
|
+
|
|
161
|
+
### To 0.2: the second factor and attempts
|
|
162
|
+
|
|
163
|
+
Every column is added nullable or with a default, so the migration rewrites
|
|
164
|
+
no row and existing users read as having no second factor:
|
|
165
|
+
|
|
166
|
+
```sql
|
|
167
|
+
ALTER TABLE "users" ADD COLUMN "second_factor_method" text;
|
|
168
|
+
ALTER TABLE "users" ADD COLUMN "second_factor_secret" text;
|
|
169
|
+
ALTER TABLE "users" ADD COLUMN "second_factor_confirmed_at" timestamp(3) with time zone;
|
|
170
|
+
ALTER TABLE "users" ADD COLUMN "second_factor_last_step" integer;
|
|
171
|
+
ALTER TABLE "tokens" ADD COLUMN "code_hash" text;
|
|
172
|
+
ALTER TABLE "tokens" ADD COLUMN "attempts" integer DEFAULT 0 NOT NULL;
|
|
173
|
+
ALTER TABLE "users" ADD CONSTRAINT "users_second_factor_whole" CHECK (("second_factor_method" is null) = ("second_factor_secret" is null) and ("second_factor_method" is not null or ("second_factor_confirmed_at" is null and "second_factor_last_step" is null)));
|
|
174
|
+
ALTER TABLE "users" ADD CONSTRAINT "users_second_factor_values" CHECK ("second_factor_method" in ('totp') and "second_factor_last_step" >= 0);
|
|
175
|
+
ALTER TABLE "tokens" ADD CONSTRAINT "tokens_attempts" CHECK ("attempts" >= 0);
|
|
176
|
+
ALTER TABLE "tokens" DROP CONSTRAINT "tokens_kind", ADD CONSTRAINT "tokens_kind" CHECK ("kind" in ('verifyEmail', 'resetPassword', 'secondFactor', 'signInCode'));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Those are the statements drizzle-kit writes from the 0.1 tables to the 0.2
|
|
180
|
+
ones, in its own order and separated by `--> statement-breakpoint`; with a
|
|
181
|
+
PostgreSQL schema of its own, every name is qualified.
|
|
182
|
+
|
|
118
183
|
## Collecting lapsed sessions
|
|
119
184
|
|
|
120
185
|
PostgreSQL has no TTL. A lapsed session is refused on every read, but it stays
|
|
@@ -149,5 +214,6 @@ const patients = row?.patients ?? 0;
|
|
|
149
214
|
```
|
|
150
215
|
|
|
151
216
|
**Read, never write.** A row written behind the store's back skips its
|
|
152
|
-
invariants: the logins' uniqueness, `version`, the password check
|
|
153
|
-
|
|
217
|
+
invariants: the logins' uniqueness, `version`, the password check, and,
|
|
218
|
+
once the second factor ships, the sealing of its secret. Write through
|
|
219
|
+
`auth` and `access`.
|
package/docs/guide/wiring.md
CHANGED
|
@@ -98,7 +98,10 @@ in `logins`. It runs through `@nxgt/drizzle`'s `withTransaction`, inside
|
|
|
98
98
|
the method; the core never opens one. Every other write is a single
|
|
99
99
|
statement. `consumeToken` is one statement, `with before as (select … for update) update … from before`: of
|
|
100
100
|
twenty concurrent redemptions of one token, exactly one sees `spentAt: null`.
|
|
101
|
-
|
|
101
|
+
`countAttempt` is one `update … set attempts = attempts + 1 … returning`
|
|
102
|
+
under the row's lock, followed by a plain read only when it matched nothing — a spent token, or none:
|
|
103
|
+
twenty concurrent attempts answer twenty distinct counts. Both are measured
|
|
104
|
+
on PostgreSQL 17 on every CI run.
|
|
102
105
|
|
|
103
106
|
Every method of the port is implemented, **including the optional
|
|
104
107
|
`deleteExpiredSessions`**. PostgreSQL has no TTL, so
|
package/docs/roadmap.md
CHANGED
|
@@ -36,6 +36,11 @@ Nothing yet.
|
|
|
36
36
|
|
|
37
37
|
## Shipped
|
|
38
38
|
|
|
39
|
+
- **A second factor on a user, and attempts counted on a token, v0.2.0** —
|
|
40
|
+
for `@nxgt/janus` 0.4: `users` gains the four `second_factor_*` columns,
|
|
41
|
+
`tokens` gains `code_hash` and `attempts`, and `countAttempt` counts an
|
|
42
|
+
attempt at a code in one conditional `update`. A migration, generated by
|
|
43
|
+
drizzle-kit, adds them without rewriting a row.
|
|
39
44
|
- **The first release, v0.1.0.** Both sides of `@nxgt/janus` over one
|
|
40
45
|
PostgreSQL database, on Drizzle; the adapter passes both conformance suites
|
|
41
46
|
on PostgreSQL 17 and PGlite. `defineJanusTables({ schema? })` puts the tables
|
package/docs/troubleshooting.md
CHANGED
|
@@ -32,6 +32,7 @@ How PostgreSQL's errors become the port's:
|
|
|
32
32
|
|
|
33
33
|
**Setup**
|
|
34
34
|
- [`STORE_FAILED` caused by `relation "users" does not exist`](#store_failed-caused-by-relation-users-does-not-exist)
|
|
35
|
+
- [`STORE_FAILED` caused by `column "second_factor_method" does not exist`](#store_failed-caused-by-column-second_factor_method-does-not-exist)
|
|
35
36
|
- [`STORE_FAILED` caused by `column "type" does not exist`](#store_failed-caused-by-column-type-does-not-exist)
|
|
36
37
|
- [`schema "janus" does not exist` while migrating](#schema-janus-does-not-exist-while-migrating)
|
|
37
38
|
- [`syntax error at or near "NULLS"` while migrating](#syntax-error-at-or-near-nulls-while-migrating)
|
|
@@ -183,6 +184,26 @@ With a PostgreSQL schema, pass the adapter the tables built in it:
|
|
|
183
184
|
createDrizzleAdapter(db, { tables: janusTables }); // from the schema file
|
|
184
185
|
```
|
|
185
186
|
|
|
187
|
+
### `STORE_FAILED` caused by `column "second_factor_method" does not exist`
|
|
188
|
+
|
|
189
|
+
Also `column "code_hash" does not exist`, or `column "attempts" does not
|
|
190
|
+
exist`. The code is `42703`.
|
|
191
|
+
|
|
192
|
+
**When:** after upgrading to 0.2, on the first sign-up, sign-in or one-time
|
|
193
|
+
token.
|
|
194
|
+
|
|
195
|
+
**Why:** the tables gained six columns — four `second_factor_*` on `users`,
|
|
196
|
+
`code_hash` and `attempts` on `tokens` — and the migration that adds them was
|
|
197
|
+
not generated, or not applied.
|
|
198
|
+
|
|
199
|
+
**Fix:** generate and apply it with Janus's own drizzle-kit config, before the
|
|
200
|
+
new version serves requests. See [upgrading](guide/migrations.md#upgrading).
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
bunx drizzle-kit generate --config drizzle.janus.config.ts
|
|
204
|
+
bunx drizzle-kit migrate --config drizzle.janus.config.ts
|
|
205
|
+
```
|
|
206
|
+
|
|
186
207
|
### `STORE_FAILED` caused by `column "type" does not exist`
|
|
187
208
|
|
|
188
209
|
Also `column "type" of relation "users" does not exist`, or another column of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/janus-drizzle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The PostgreSQL adapter for @nxgt/janus on Drizzle and @nxgt/drizzle: users, sessions, one-time tokens and permission tuples",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@electric-sql/pglite": "0.5.8",
|
|
53
53
|
"@nxgt/drizzle": "0.6.1",
|
|
54
|
-
"@nxgt/janus": "^0.
|
|
54
|
+
"@nxgt/janus": "^0.4.0",
|
|
55
55
|
"@types/bun": "^1.4.2",
|
|
56
56
|
"@types/pg": "^8.23.1",
|
|
57
57
|
"drizzle-kit": "1.0.0-rc.4",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@nxgt/drizzle": ">=0.6.1 <1",
|
|
65
|
-
"@nxgt/janus": "^0.
|
|
65
|
+
"@nxgt/janus": "^0.4.0",
|
|
66
66
|
"drizzle-orm": ">=1.0.0-rc.4 <2",
|
|
67
67
|
"typescript": "^6.0.3"
|
|
68
68
|
}
|