@cogenta/auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/audit.d.ts +17 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +113 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/credentials.d.ts +33 -0
  6. package/dist/credentials.d.ts.map +1 -0
  7. package/dist/credentials.js +95 -0
  8. package/dist/credentials.js.map +1 -0
  9. package/dist/index.d.ts +32 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +23 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/login.d.ts +66 -0
  14. package/dist/login.d.ts.map +1 -0
  15. package/dist/login.js +300 -0
  16. package/dist/login.js.map +1 -0
  17. package/dist/mfa.d.ts +4 -0
  18. package/dist/mfa.d.ts.map +1 -0
  19. package/dist/mfa.js +28 -0
  20. package/dist/mfa.js.map +1 -0
  21. package/dist/password.d.ts +11 -0
  22. package/dist/password.d.ts.map +1 -0
  23. package/dist/password.js +108 -0
  24. package/dist/password.js.map +1 -0
  25. package/dist/rate-limit.d.ts +10 -0
  26. package/dist/rate-limit.d.ts.map +1 -0
  27. package/dist/rate-limit.js +47 -0
  28. package/dist/rate-limit.js.map +1 -0
  29. package/dist/sessions.d.ts +20 -0
  30. package/dist/sessions.d.ts.map +1 -0
  31. package/dist/sessions.js +82 -0
  32. package/dist/sessions.js.map +1 -0
  33. package/dist/store.d.ts +30 -0
  34. package/dist/store.d.ts.map +1 -0
  35. package/dist/store.js +27 -0
  36. package/dist/store.js.map +1 -0
  37. package/dist/tables.d.ts +17 -0
  38. package/dist/tables.d.ts.map +1 -0
  39. package/dist/tables.js +103 -0
  40. package/dist/tables.js.map +1 -0
  41. package/dist/totp.d.ts +22 -0
  42. package/dist/totp.d.ts.map +1 -0
  43. package/dist/totp.js +115 -0
  44. package/dist/totp.js.map +1 -0
  45. package/dist/types.d.ts +64 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +11 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/users.d.ts +12 -0
  50. package/dist/users.d.ts.map +1 -0
  51. package/dist/users.js +69 -0
  52. package/dist/users.js.map +1 -0
  53. package/dist/webauthn.d.ts +40 -0
  54. package/dist/webauthn.d.ts.map +1 -0
  55. package/dist/webauthn.js +81 -0
  56. package/dist/webauthn.js.map +1 -0
  57. package/package.json +44 -0
@@ -0,0 +1,82 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { identifier, newId, sql } from '@cogenta/core';
3
+ import { TABLES } from './tables.js';
4
+ const TOKEN_BYTES = 32;
5
+ const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days, sliding on use.
6
+ /**
7
+ * A session token is opaque random bytes, never a JWT.
8
+ *
9
+ * A JWT is a claim the server made and stopped being able to take back the
10
+ * moment it left the process — revoking one means keeping a blocklist anyway,
11
+ * which is the session table this already is. An opaque token looked up on
12
+ * every request costs one indexed read and buys back revocation that actually
13
+ * revokes, including "sign out of every device".
14
+ */
15
+ function issueToken() {
16
+ return randomBytes(TOKEN_BYTES).toString('base64url');
17
+ }
18
+ /** Stored hashed, like a password, so a leaked table hands out nothing live. */
19
+ function hashToken(token) {
20
+ return createHash('sha256').update(token).digest('base64url');
21
+ }
22
+ function fromRow(row) {
23
+ return {
24
+ id: row.id,
25
+ userId: row.user_id,
26
+ createdAt: row.created_at,
27
+ expiresAt: row.expires_at,
28
+ lastSeenAt: row.last_seen_at,
29
+ label: row.label ?? undefined,
30
+ };
31
+ }
32
+ export function createSessionStore(db, now = Date.now) {
33
+ const table = identifier(TABLES.sessions, db.dialect);
34
+ return {
35
+ create: async (userId, options) => {
36
+ const token = issueToken();
37
+ const id = newId(now);
38
+ const created = new Date(now()).toISOString();
39
+ const expires = new Date(now() + (options?.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
40
+ await db.query(sql `
41
+ insert into ${table} (id, user_id, token_hash, label, created_at, expires_at, last_seen_at, revoked)
42
+ values (${id}, ${userId}, ${hashToken(token)}, ${options?.label ?? null}, ${created}, ${expires}, ${created}, ${false})`);
43
+ return {
44
+ id,
45
+ userId,
46
+ token,
47
+ createdAt: created,
48
+ expiresAt: expires,
49
+ lastSeenAt: created,
50
+ label: options?.label,
51
+ };
52
+ },
53
+ resolve: async (token) => {
54
+ // Looked up by the hash, never by the token — the table never contains
55
+ // anything that would let a database read alone impersonate a session.
56
+ const targetHash = hashToken(token);
57
+ const result = await db.query(sql `select * from ${table} where token_hash = ${targetHash}`);
58
+ const row = result.rows[0];
59
+ if (row === undefined)
60
+ return null;
61
+ if (row.revoked)
62
+ return null;
63
+ const nowMs = now();
64
+ if (new Date(row.expires_at).getTime() <= nowMs)
65
+ return null;
66
+ const lastSeen = new Date(nowMs).toISOString();
67
+ await db.query(sql `update ${table} set last_seen_at = ${lastSeen} where id = ${row.id}`);
68
+ return fromRow({ ...row, last_seen_at: lastSeen });
69
+ },
70
+ list: async (userId) => {
71
+ const result = await db.query(sql `select * from ${table} where user_id = ${userId} and revoked = ${false} order by last_seen_at desc`);
72
+ return result.rows.map(fromRow);
73
+ },
74
+ revoke: async (sessionId) => {
75
+ await db.query(sql `update ${table} set revoked = ${true} where id = ${sessionId}`);
76
+ },
77
+ revokeAll: async (userId) => {
78
+ await db.query(sql `update ${table} set revoked = ${true} where user_id = ${userId}`);
79
+ },
80
+ };
81
+ }
82
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { DatabaseHandle } from '@cogenta/core';
2
+ import type { CollectionDefinition } from '@cogenta/schema';
3
+ import { createAuditLog } from './audit.js';
4
+ import { createCredentialStore } from './credentials.js';
5
+ import { createAuthService } from './login.js';
6
+ import { createRateLimiter } from './rate-limit.js';
7
+ import { createSessionStore } from './sessions.js';
8
+ import { createUserStore } from './users.js';
9
+ import type { WebAuthnConfig } from './webauthn.js';
10
+ export interface AuthStoreOptions {
11
+ readonly db: DatabaseHandle;
12
+ readonly signingKey: string;
13
+ readonly collections: readonly CollectionDefinition[];
14
+ /** Shown in the authenticator app next to the account name. Defaults to "Cogenta". */
15
+ readonly issuer?: string;
16
+ /** Absent means passkeys are off. */
17
+ readonly webauthn?: WebAuthnConfig;
18
+ readonly now?: () => number;
19
+ }
20
+ /** Every piece of this package, wired together against one connection. */
21
+ export interface AuthStore {
22
+ readonly users: ReturnType<typeof createUserStore>;
23
+ readonly credentials: ReturnType<typeof createCredentialStore>;
24
+ readonly sessions: ReturnType<typeof createSessionStore>;
25
+ readonly audit: ReturnType<typeof createAuditLog>;
26
+ readonly rateLimit: ReturnType<typeof createRateLimiter>;
27
+ readonly login: ReturnType<typeof createAuthService>;
28
+ }
29
+ export declare function createAuthStore(options: AuthStoreOptions): Promise<AuthStore>;
30
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;IAClD,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAA;IAC9D,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA;IACjD,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;CACrD;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAmBnF"}
package/dist/store.js ADDED
@@ -0,0 +1,27 @@
1
+ import { createAuditLog } from './audit.js';
2
+ import { createCredentialStore } from './credentials.js';
3
+ import { createAuthService } from './login.js';
4
+ import { createRateLimiter } from './rate-limit.js';
5
+ import { createSessionStore } from './sessions.js';
6
+ import { ensureAuthTables } from './tables.js';
7
+ import { createUserStore } from './users.js';
8
+ export async function createAuthStore(options) {
9
+ await ensureAuthTables(options.db);
10
+ const now = options.now ?? Date.now;
11
+ return {
12
+ users: createUserStore(options.db, now),
13
+ credentials: createCredentialStore(options.db, now),
14
+ sessions: createSessionStore(options.db, now),
15
+ audit: createAuditLog(options.db, now),
16
+ rateLimit: createRateLimiter(options.db, now),
17
+ login: createAuthService({
18
+ db: options.db,
19
+ signingKey: options.signingKey,
20
+ collections: options.collections,
21
+ now,
22
+ ...(options.issuer === undefined ? {} : { issuer: options.issuer }),
23
+ ...(options.webauthn === undefined ? {} : { webauthn: options.webauthn }),
24
+ }),
25
+ };
26
+ }
27
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAwB5C,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAyB;IAC7D,MAAM,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IAEnC,OAAO;QACL,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QACvC,WAAW,EAAE,qBAAqB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QACnD,QAAQ,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QAC7C,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QACtC,SAAS,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QAC7C,KAAK,EAAE,iBAAiB,CAAC;YACvB,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,GAAG;YACH,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;YACnE,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;SAC1E,CAAC;KACH,CAAA;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ export declare const TABLES: {
3
+ readonly users: 'cogenta_users';
4
+ readonly credentials: 'cogenta_credentials';
5
+ readonly sessions: 'cogenta_sessions';
6
+ readonly loginAttempts: 'cogenta_login_attempts';
7
+ readonly auditLog: 'cogenta_audit_log';
8
+ };
9
+ /**
10
+ * Every table this package owns.
11
+ *
12
+ * Run once, at startup, the same way the migration engine and the queue driver
13
+ * do it: `create table if not exists`, so a fresh install and an upgrade take
14
+ * the same path.
15
+ */
16
+ export declare function ensureAuthTables(db: DatabaseHandle): Promise<void>;
17
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,MAAM;aACjB,KAAK,EAAE,eAAe;aACtB,WAAW,EAAE,qBAAqB;aAClC,QAAQ,EAAE,kBAAkB;aAC5B,aAAa,EAAE,wBAAwB;aACvC,QAAQ,EAAE,mBAAmB;CACrB,CAAA;AAWV;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFxE"}
package/dist/tables.js ADDED
@@ -0,0 +1,103 @@
1
+ import { identifier, sql, unsafeRaw, } from '@cogenta/core';
2
+ export const TABLES = {
3
+ users: 'cogenta_users',
4
+ credentials: 'cogenta_credentials',
5
+ sessions: 'cogenta_sessions',
6
+ loginAttempts: 'cogenta_login_attempts',
7
+ auditLog: 'cogenta_audit_log',
8
+ };
9
+ /** `varchar` on Postgres/MySQL, `text` on SQLite — encapsulated once, here. */
10
+ function textColumn(dialect, length) {
11
+ return unsafeRaw(dialect === 'sqlite' ? 'text' : `varchar(${length})`);
12
+ }
13
+ function booleanColumn(dialect) {
14
+ return unsafeRaw(dialect === 'postgres' ? 'boolean' : 'tinyint');
15
+ }
16
+ /**
17
+ * Every table this package owns.
18
+ *
19
+ * Run once, at startup, the same way the migration engine and the queue driver
20
+ * do it: `create table if not exists`, so a fresh install and an upgrade take
21
+ * the same path.
22
+ */
23
+ export async function ensureAuthTables(db) {
24
+ const d = db.dialect;
25
+ const users = identifier(TABLES.users, d);
26
+ const credentials = identifier(TABLES.credentials, d);
27
+ const sessions = identifier(TABLES.sessions, d);
28
+ const loginAttempts = identifier(TABLES.loginAttempts, d);
29
+ const auditLog = identifier(TABLES.auditLog, d);
30
+ const t512 = textColumn(d, 512);
31
+ const t255 = textColumn(d, 255);
32
+ const t64 = textColumn(d, 64);
33
+ const bool = booleanColumn(d);
34
+ await db.query(sql `
35
+ create table if not exists ${users} (
36
+ id ${t64} not null primary key,
37
+ email ${t255} not null unique,
38
+ -- An open set of role names (contract A), stored as JSON text on every
39
+ -- dialect for the same reason the migration engine stores timestamps as
40
+ -- text: one representation that means the same thing everywhere, rather
41
+ -- than a native array type only Postgres has.
42
+ roles text not null,
43
+ status ${t64} not null,
44
+ created_at ${t64} not null,
45
+ updated_at ${t64} not null
46
+ )`);
47
+ await db.query(sql `
48
+ create table if not exists ${credentials} (
49
+ id ${t64} not null primary key,
50
+ user_id ${t64} not null,
51
+ kind ${t64} not null,
52
+ -- Kind-specific, never interpreted at this layer:
53
+ -- password: { hash }
54
+ -- totp: { secret, verified }
55
+ -- webauthn: { credentialId, publicKey, counter, transports, label }
56
+ data text not null,
57
+ created_at ${t64} not null
58
+ )`);
59
+ await db.query(sql `
60
+ create table if not exists ${sessions} (
61
+ id ${t64} not null primary key,
62
+ user_id ${t64} not null,
63
+ -- The bearer token itself is never stored, only its hash — the same
64
+ -- reasoning as a password, applied to the thing that stands in for one
65
+ -- after login. A leaked database does not hand out live sessions.
66
+ token_hash ${t512} not null unique,
67
+ label ${t255},
68
+ created_at ${t64} not null,
69
+ expires_at ${t64} not null,
70
+ last_seen_at ${t64} not null,
71
+ revoked ${bool} not null
72
+ )`);
73
+ await db.query(sql `
74
+ create table if not exists ${loginAttempts} (
75
+ id ${t64} not null primary key,
76
+ -- Keyed by subject (email or IP), not by user id: an attacker probing a
77
+ -- nonexistent email must be rate-limited too, or enumeration is free.
78
+ subject ${t255} not null,
79
+ at ${t64} not null
80
+ )`);
81
+ await db.query(sql `
82
+ create table if not exists ${auditLog} (
83
+ id ${t64} not null primary key,
84
+ at ${t64} not null,
85
+ actor_id ${t64},
86
+ actor_roles text not null,
87
+ action ${t255} not null,
88
+ collection_name ${t255},
89
+ entry_id ${t64},
90
+ diff text,
91
+ hash ${t64} not null,
92
+ previous_hash ${t64}
93
+ )`);
94
+ await createIndexIfMissing(db, 'cogenta_credentials_user', credentials, sql `(user_id)`);
95
+ await createIndexIfMissing(db, 'cogenta_sessions_user', sessions, sql `(user_id)`);
96
+ await createIndexIfMissing(db, 'cogenta_login_attempts_subject_at', loginAttempts, sql `(subject, at)`);
97
+ }
98
+ async function createIndexIfMissing(db, name, table, columns) {
99
+ await db
100
+ .query(sql `create index ${identifier(name, db.dialect)} on ${table} ${columns}`)
101
+ .catch(() => undefined); // already there — no portable "if not exists" for indexes
102
+ }
103
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"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"}
package/dist/totp.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export declare function generateTotpSecret(): string;
2
+ /**
3
+ * The `otpauth://` URI an authenticator app scans as a QR code.
4
+ *
5
+ * `issuer` and `label` are URL-encoded independently: a site name or a user
6
+ * email containing `&` or `:` must not be able to smuggle extra URI parameters
7
+ * into what the app parses.
8
+ */
9
+ export declare function totpUri(secret: string, issuer: string, label: string): string;
10
+ export interface VerifyTotpOptions {
11
+ /** Epoch seconds. Injected so a test never races the real clock. */
12
+ readonly now?: number;
13
+ /**
14
+ * Time steps of drift accepted either side of now, to absorb clock skew
15
+ * between the server and a phone that has not synced in a while. 1 means
16
+ * the previous, current and next 30-second window all validate.
17
+ */
18
+ readonly windowSteps?: number;
19
+ }
20
+ export declare function verifyTotp(token: string, secret: string, options?: VerifyTotpOptions): boolean;
21
+ export declare function assertTotpSecretFormat(secret: string): void;
22
+ //# sourceMappingURL=totp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../src/totp.ts"],"names":[],"mappings":"AAgBA,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAS7E;AAiBD,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAmBT;AA8CD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQ3D"}
package/dist/totp.js ADDED
@@ -0,0 +1,115 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { CogentaError } from '@cogenta/core';
3
+ /**
4
+ * TOTP, RFC 6238 over HOTP, RFC 4226 — hand-written rather than a dependency.
5
+ *
6
+ * It is forty lines of HMAC and modular arithmetic with no ambiguity in the
7
+ * spec and no ceremony to get subtly wrong, which is the opposite of WebAuthn:
8
+ * that one is a dependency for exactly the reasons this one is not.
9
+ */
10
+ const DIGITS = 6;
11
+ const PERIOD_SECONDS = 30;
12
+ const ALGORITHM = 'sha1'; // What every authenticator app (contract: RFC 6238 default) expects.
13
+ const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
14
+ export function generateTotpSecret() {
15
+ return base32Encode(randomBytes(20));
16
+ }
17
+ /**
18
+ * The `otpauth://` URI an authenticator app scans as a QR code.
19
+ *
20
+ * `issuer` and `label` are URL-encoded independently: a site name or a user
21
+ * email containing `&` or `:` must not be able to smuggle extra URI parameters
22
+ * into what the app parses.
23
+ */
24
+ export function totpUri(secret, issuer, label) {
25
+ const params = new URLSearchParams({
26
+ secret,
27
+ issuer,
28
+ algorithm: 'SHA1',
29
+ digits: String(DIGITS),
30
+ period: String(PERIOD_SECONDS),
31
+ });
32
+ return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(label)}?${params.toString()}`;
33
+ }
34
+ function hotp(secret, counter) {
35
+ const counterBytes = Buffer.alloc(8);
36
+ counterBytes.writeBigUInt64BE(counter);
37
+ const digest = createHmac(ALGORITHM, secret).update(counterBytes).digest();
38
+ const offset = (digest.at(-1) ?? 0) & 0x0f;
39
+ const truncated = ((digest[offset] ?? 0) & 0x7f) * 2 ** 24 +
40
+ ((digest[offset + 1] ?? 0) & 0xff) * 2 ** 16 +
41
+ ((digest[offset + 2] ?? 0) & 0xff) * 2 ** 8 +
42
+ ((digest[offset + 3] ?? 0) & 0xff);
43
+ return String(truncated % 10 ** DIGITS).padStart(DIGITS, '0');
44
+ }
45
+ export function verifyTotp(token, secret, options = {}) {
46
+ if (!/^\d{6}$/.test(token))
47
+ return false;
48
+ const key = base32Decode(secret);
49
+ if (key === null)
50
+ return false;
51
+ const now = options.now ?? Math.floor(Date.now() / 1000);
52
+ const windowSteps = options.windowSteps ?? 1;
53
+ const currentStep = BigInt(Math.floor(now / PERIOD_SECONDS));
54
+ // Every candidate in the window is checked — never short-circuited on the
55
+ // first comparison — and each comparison is constant-time, so accepting on
56
+ // the third of three steps takes the same time as rejecting on the first.
57
+ let matched = false;
58
+ for (let offset = -windowSteps; offset <= windowSteps; offset += 1) {
59
+ const candidate = hotp(key, currentStep + BigInt(offset));
60
+ if (constantTimeEquals(candidate, token))
61
+ matched = true;
62
+ }
63
+ return matched;
64
+ }
65
+ function constantTimeEquals(a, b) {
66
+ const bufferA = Buffer.from(a);
67
+ const bufferB = Buffer.from(b);
68
+ if (bufferA.length !== bufferB.length)
69
+ return false;
70
+ return timingSafeEqual(bufferA, bufferB);
71
+ }
72
+ function base32Encode(bytes) {
73
+ let bits = 0;
74
+ let value = 0;
75
+ let output = '';
76
+ for (const byte of bytes) {
77
+ value = (value << 8) | byte;
78
+ bits += 8;
79
+ while (bits >= 5) {
80
+ output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
81
+ bits -= 5;
82
+ }
83
+ }
84
+ if (bits > 0)
85
+ output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
86
+ return output;
87
+ }
88
+ function base32Decode(text) {
89
+ const normalised = text.toUpperCase().replace(/=+$/u, '');
90
+ if (normalised.length === 0 || !/^[A-Z2-7]+$/u.test(normalised))
91
+ return null;
92
+ const bytes = [];
93
+ let bits = 0;
94
+ let value = 0;
95
+ for (const char of normalised) {
96
+ const index = BASE32_ALPHABET.indexOf(char);
97
+ value = (value << 5) | index;
98
+ bits += 5;
99
+ if (bits >= 8) {
100
+ bytes.push((value >>> (bits - 8)) & 0xff);
101
+ bits -= 8;
102
+ }
103
+ }
104
+ return Buffer.from(bytes);
105
+ }
106
+ export function assertTotpSecretFormat(secret) {
107
+ if (base32Decode(secret) === null) {
108
+ throw new CogentaError({
109
+ code: 'AUTH_TOTP_INVALID',
110
+ message: 'A TOTP secret must be base32 text.',
111
+ hint: 'Generate one with generateTotpSecret() rather than constructing it by hand.',
112
+ });
113
+ }
114
+ }
115
+ //# sourceMappingURL=totp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.js","sourceRoot":"","sources":["../src/totp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C;;;;;;GAMG;AAEH,MAAM,MAAM,GAAG,CAAC,CAAA;AAChB,MAAM,cAAc,GAAG,EAAE,CAAA;AACzB,MAAM,SAAS,GAAG,MAAM,CAAA,CAAC,qEAAqE;AAC9F,MAAM,eAAe,GAAG,kCAAkC,CAAA;AAE1D,MAAM,UAAU,kBAAkB;IAChC,OAAO,YAAY,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAA;AACtC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,MAAc,EAAE,KAAa;IACnE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,MAAM;QACN,MAAM;QACN,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;KAC/B,CAAC,CAAA;IACF,OAAO,kBAAkB,kBAAkB,CAAC,MAAM,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAA;AACzG,CAAC;AAED,SAAS,IAAI,CAAC,MAAc,EAAE,OAAe;IAC3C,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACpC,YAAY,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAEtC,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAA;IAC1E,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IAC1C,MAAM,SAAS,GACb,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QACxC,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAC5C,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;QAC3C,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAEpC,OAAO,MAAM,CAAC,SAAS,GAAG,EAAE,IAAI,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAC/D,CAAC;AAaD,MAAM,UAAU,UAAU,CACxB,KAAa,EACb,MAAc,EACd,OAAO,GAAsB,EAAE;IAE/B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAExC,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IAChC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAE9B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACxD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,CAAA;IAC5C,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,cAAc,CAAC,CAAC,CAAA;IAE5D,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,KAAK,IAAI,MAAM,GAAG,CAAC,WAAW,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC;QACnE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QACzD,IAAI,kBAAkB,CAAC,SAAS,EAAE,KAAK,CAAC;YAAE,OAAO,GAAG,IAAI,CAAA;IAC1D,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,kBAAkB,CAAC,CAAS,EAAE,CAAS;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC9B,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACnD,OAAO,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AAC1C,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,MAAM,GAAG,EAAE,CAAA;IAEf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;QAC3B,IAAI,IAAI,CAAC,CAAA;QACT,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC;YACjB,MAAM,IAAI,eAAe,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;YACtD,IAAI,IAAI,CAAC,CAAA;QACX,CAAC;IACH,CAAC;IACD,IAAI,IAAI,GAAG,CAAC;QAAE,MAAM,IAAI,eAAe,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;IACnE,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAE5E,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3C,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,KAAK,CAAA;QAC5B,IAAI,IAAI,CAAC,CAAA;QACT,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;YACzC,IAAI,IAAI,CAAC,CAAA;QACX,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,mBAAmB;YACzB,OAAO,EAAE,oCAAoC;YAC7C,IAAI,EAAE,6EAA6E;SACpF,CAAC,CAAA;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Identity, sessions and credentials.
3
+ *
4
+ * Contract A deliberately stops at "roles are an open set of names declared in
5
+ * a collection's permissions" and says who attaches a role to a user is L2's
6
+ * concern (ADR-0014's neighbour decision, recorded in `docs/lots/L2-admin.md`).
7
+ * This package is that concern: it is what turns a role name into an actual
8
+ * signed-in person.
9
+ */
10
+ export declare const CREDENTIAL_KINDS: readonly ['password', 'totp', 'webauthn'];
11
+ export type CredentialKind = (typeof CREDENTIAL_KINDS)[number];
12
+ export interface User {
13
+ readonly id: string;
14
+ readonly email: string;
15
+ /** An open set of names, exactly as contract A's collection permissions expect. */
16
+ readonly roles: readonly string[];
17
+ readonly status: 'active' | 'disabled';
18
+ readonly createdAt: string;
19
+ readonly updatedAt: string;
20
+ }
21
+ export interface CreateUserInput {
22
+ readonly email: string;
23
+ readonly roles: readonly string[];
24
+ }
25
+ export interface Session {
26
+ readonly id: string;
27
+ readonly userId: string;
28
+ readonly createdAt: string;
29
+ readonly expiresAt: string;
30
+ readonly lastSeenAt: string;
31
+ /** Free text, shown in "your sessions" so a person can recognise a device. */
32
+ readonly label: string | undefined;
33
+ }
34
+ /**
35
+ * A session as returned once, at creation. `token` is the bearer credential; it
36
+ * is never stored — only its hash is (the same reasoning as a password, applied
37
+ * to the thing that stands in for one after login).
38
+ */
39
+ export interface IssuedSession extends Session {
40
+ readonly token: string;
41
+ }
42
+ export interface AuditEntry {
43
+ readonly id: string;
44
+ readonly at: string;
45
+ readonly actorId: string | null;
46
+ /** The roles the actor held at the time, not what they hold now. */
47
+ readonly actorRoles: readonly string[];
48
+ readonly action: string;
49
+ readonly collection: string | null;
50
+ readonly entryId: string | null;
51
+ readonly diff: Readonly<Record<string, unknown>> | null;
52
+ /** Chains to the previous entry's hash. The first entry chains to null. */
53
+ readonly hash: string;
54
+ readonly previousHash: string | null;
55
+ }
56
+ export interface RecordAuditInput {
57
+ readonly actorId: string | null;
58
+ readonly actorRoles: readonly string[];
59
+ readonly action: string;
60
+ readonly collection?: string | undefined;
61
+ readonly entryId?: string | undefined;
62
+ readonly diff?: Readonly<Record<string, unknown>> | undefined;
63
+ }
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,gBAAgB,YAAI,UAAU,EAAE,MAAM,EAAE,UAAU,CAAU,CAAA;AACzE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAA;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAA;IACvD,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;CAC9D"}
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Identity, sessions and credentials.
3
+ *
4
+ * Contract A deliberately stops at "roles are an open set of names declared in
5
+ * a collection's permissions" and says who attaches a role to a user is L2's
6
+ * concern (ADR-0014's neighbour decision, recorded in `docs/lots/L2-admin.md`).
7
+ * This package is that concern: it is what turns a role name into an actual
8
+ * signed-in person.
9
+ */
10
+ export const CREDENTIAL_KINDS = ['password', 'totp', 'webauthn'];
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,CAAU,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ import type { CreateUserInput, User } from './types.js';
3
+ export interface UserStore {
4
+ create(input: CreateUserInput): Promise<User>;
5
+ byEmail(email: string): Promise<User | null>;
6
+ byId(id: string): Promise<User | null>;
7
+ setRoles(id: string, roles: readonly string[]): Promise<void>;
8
+ setStatus(id: string, status: User['status']): Promise<void>;
9
+ list(): Promise<readonly User[]>;
10
+ }
11
+ export declare function createUserStore(db: DatabaseHandle, now?: () => number): UserStore;
12
+ //# sourceMappingURL=users.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../src/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAEzF,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AA0BvD,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IAC5C,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7D,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,IAAI,IAAI,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAA;CACjC;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,SAAS,CAgE3F"}
package/dist/users.js ADDED
@@ -0,0 +1,69 @@
1
+ import { CogentaError, identifier, newId, sql } from '@cogenta/core';
2
+ import { TABLES } from './tables.js';
3
+ function fromRow(row) {
4
+ return {
5
+ id: row.id,
6
+ email: row.email,
7
+ roles: JSON.parse(row.roles),
8
+ status: row.status,
9
+ createdAt: row.created_at,
10
+ updatedAt: row.updated_at,
11
+ };
12
+ }
13
+ function normaliseEmail(email) {
14
+ return email.trim().toLowerCase();
15
+ }
16
+ export function createUserStore(db, now = Date.now) {
17
+ const table = identifier(TABLES.users, db.dialect);
18
+ return {
19
+ create: async (input) => {
20
+ const email = normaliseEmail(input.email);
21
+ const existing = await db.query(sql `select id from ${table} where email = ${email}`);
22
+ if (existing.rows.length > 0) {
23
+ throw new CogentaError({
24
+ code: 'AUTH_USER_EXISTS',
25
+ message: `A user with the email ${email} already exists.`,
26
+ hint: 'Sign in with that account, or use a different email.',
27
+ });
28
+ }
29
+ const id = newId();
30
+ const timestamp = new Date(now()).toISOString();
31
+ await db.query(sql `
32
+ insert into ${table} (id, email, roles, status, created_at, updated_at)
33
+ values (${id}, ${email}, ${JSON.stringify(input.roles)}, ${'active'}, ${timestamp}, ${timestamp})`);
34
+ return {
35
+ id,
36
+ email,
37
+ roles: input.roles,
38
+ status: 'active',
39
+ createdAt: timestamp,
40
+ updatedAt: timestamp,
41
+ };
42
+ },
43
+ byEmail: async (email) => {
44
+ const result = await db.query(sql `select * from ${table} where email = ${normaliseEmail(email)}`);
45
+ const row = result.rows[0];
46
+ return row === undefined ? null : fromRow(row);
47
+ },
48
+ byId: async (id) => {
49
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
50
+ const row = result.rows[0];
51
+ return row === undefined ? null : fromRow(row);
52
+ },
53
+ setRoles: async (id, roles) => {
54
+ await db.query(sql `
55
+ update ${table} set roles = ${JSON.stringify(roles)}, updated_at = ${new Date(now()).toISOString()}
56
+ where id = ${id}`);
57
+ },
58
+ setStatus: async (id, status) => {
59
+ await db.query(sql `
60
+ update ${table} set status = ${status}, updated_at = ${new Date(now()).toISOString()}
61
+ where id = ${id}`);
62
+ },
63
+ list: async () => {
64
+ const result = await db.query(sql `select * from ${table} order by created_at asc`);
65
+ return result.rows.map(fromRow);
66
+ },
67
+ };
68
+ }
69
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../src/users.ts"],"names":[],"mappings":"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"}