@idevconn/create-icore 0.10.2 → 0.12.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 (34) hide show
  1. package/dist/cli.js +24 -2
  2. package/dist/index.cjs +23 -1
  3. package/dist/index.d.cts +2 -2
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +23 -1
  6. package/package.json +1 -1
  7. package/templates/docker-compose.yml +14 -0
  8. package/templates/libs/auth-strategies/postgres/eslint.config.mjs +32 -0
  9. package/templates/libs/auth-strategies/postgres/package.json +23 -0
  10. package/templates/libs/auth-strategies/postgres/project.json +19 -0
  11. package/templates/libs/auth-strategies/postgres/src/index.ts +3 -0
  12. package/templates/libs/auth-strategies/postgres/src/lib/__tests__/postgres-auth.contract.unit.test.ts +4 -0
  13. package/templates/libs/auth-strategies/postgres/src/lib/__tests__/postgres-auth.module.unit.test.ts +49 -0
  14. package/templates/libs/auth-strategies/postgres/src/lib/postgres-auth.module.ts +39 -0
  15. package/templates/libs/auth-strategies/postgres/src/lib/postgres-auth.strategy.ts +210 -0
  16. package/templates/libs/auth-strategies/postgres/src/lib/testing/mock-postgres-auth.ts +106 -0
  17. package/templates/libs/auth-strategies/postgres/tsconfig.json +19 -0
  18. package/templates/libs/auth-strategies/postgres/tsconfig.lib.json +24 -0
  19. package/templates/libs/auth-strategies/postgres/tsconfig.spec.json +22 -0
  20. package/templates/libs/auth-strategies/postgres/vitest.config.mts +22 -0
  21. package/templates/libs/db-strategies/postgres/eslint.config.mjs +30 -0
  22. package/templates/libs/db-strategies/postgres/package.json +19 -0
  23. package/templates/libs/db-strategies/postgres/project.json +19 -0
  24. package/templates/libs/db-strategies/postgres/src/index.ts +3 -0
  25. package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.contract.unit.test.ts +4 -0
  26. package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.module.unit.test.ts +37 -0
  27. package/templates/libs/db-strategies/postgres/src/lib/postgres-db.module.ts +33 -0
  28. package/templates/libs/db-strategies/postgres/src/lib/postgres-db.strategy.ts +139 -0
  29. package/templates/libs/db-strategies/postgres/src/lib/testing/mock-postgres.ts +94 -0
  30. package/templates/libs/db-strategies/postgres/tsconfig.json +19 -0
  31. package/templates/libs/db-strategies/postgres/tsconfig.lib.json +24 -0
  32. package/templates/libs/db-strategies/postgres/tsconfig.spec.json +22 -0
  33. package/templates/libs/db-strategies/postgres/vitest.config.mts +22 -0
  34. package/templates/tsconfig.base.json +3 -1
package/dist/cli.js CHANGED
@@ -224,6 +224,7 @@ Re-run with @latest to refresh:
224
224
  { value: "supabase", label: "Supabase" },
225
225
  { value: "firebase", label: "Firebase" },
226
226
  { value: "mongodb", label: "MongoDB (Custom Auth)" },
227
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js + bcrypt + JWT)" },
227
228
  { value: "none", label: "None \u2014 no login, open API (simple SPA)" }
228
229
  ]
229
230
  });
@@ -233,7 +234,8 @@ Re-run with @latest to refresh:
233
234
  options: [
234
235
  { value: "supabase", label: "Supabase Postgres" },
235
236
  { value: "firebase", label: "Firestore" },
236
- { value: "mongodb", label: "MongoDB" }
237
+ { value: "mongodb", label: "MongoDB" },
238
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
237
239
  ],
238
240
  initialValue: authProvider
239
241
  });
@@ -410,7 +412,7 @@ async function rewriteRootPackageJson(targetDir, opts) {
410
412
  const pkg = JSON.parse(raw);
411
413
  pkg["name"] = opts.projectName;
412
414
  pkg["version"] = "0.0.1";
413
- pkg["icoreVersion"] = true ? "0.10.2" : "unknown";
415
+ pkg["icoreVersion"] = true ? "0.12.0" : "unknown";
414
416
  pkg["private"] = true;
415
417
  delete pkg.description;
416
418
  const transportDeps = TRANSPORT_DEPS[opts.transport];
@@ -1476,6 +1478,16 @@ var MANIFEST = {
1476
1478
  deps: { mongoose: "^9.6.3" },
1477
1479
  tsPaths: { "@icore/auth-mongodb": ["libs/auth-strategies/mongodb/src/index.ts"] },
1478
1480
  nestModule: { importFrom: "@icore/auth-mongodb", symbol: "MongoDbAuthModule", into: "auth" }
1481
+ },
1482
+ postgres: {
1483
+ libDirs: ["libs/auth-strategies/postgres"],
1484
+ deps: { postgres: "^3", bcrypt: "^6", jsonwebtoken: "^9" },
1485
+ tsPaths: { "@icore/auth-postgres": ["libs/auth-strategies/postgres/src/index.ts"] },
1486
+ nestModule: {
1487
+ importFrom: "@icore/auth-postgres",
1488
+ symbol: "PostgresAuthModule",
1489
+ into: "auth"
1490
+ }
1479
1491
  }
1480
1492
  },
1481
1493
  storage: {
@@ -1538,6 +1550,16 @@ var MANIFEST = {
1538
1550
  deps: { mongoose: "^9.6.3" },
1539
1551
  tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
1540
1552
  nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
1553
+ },
1554
+ postgres: {
1555
+ libDirs: ["libs/db-strategies/postgres"],
1556
+ deps: { postgres: "^3" },
1557
+ tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
1558
+ nestModule: {
1559
+ importFrom: "@icore/db-postgres",
1560
+ symbol: "PostgresDbModule",
1561
+ into: "notes"
1562
+ }
1541
1563
  }
1542
1564
  },
1543
1565
  feature: {
package/dist/index.cjs CHANGED
@@ -1187,6 +1187,16 @@ var MANIFEST = {
1187
1187
  deps: { mongoose: "^9.6.3" },
1188
1188
  tsPaths: { "@icore/auth-mongodb": ["libs/auth-strategies/mongodb/src/index.ts"] },
1189
1189
  nestModule: { importFrom: "@icore/auth-mongodb", symbol: "MongoDbAuthModule", into: "auth" }
1190
+ },
1191
+ postgres: {
1192
+ libDirs: ["libs/auth-strategies/postgres"],
1193
+ deps: { postgres: "^3", bcrypt: "^6", jsonwebtoken: "^9" },
1194
+ tsPaths: { "@icore/auth-postgres": ["libs/auth-strategies/postgres/src/index.ts"] },
1195
+ nestModule: {
1196
+ importFrom: "@icore/auth-postgres",
1197
+ symbol: "PostgresAuthModule",
1198
+ into: "auth"
1199
+ }
1190
1200
  }
1191
1201
  },
1192
1202
  storage: {
@@ -1249,6 +1259,16 @@ var MANIFEST = {
1249
1259
  deps: { mongoose: "^9.6.3" },
1250
1260
  tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
1251
1261
  nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
1262
+ },
1263
+ postgres: {
1264
+ libDirs: ["libs/db-strategies/postgres"],
1265
+ deps: { postgres: "^3" },
1266
+ tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
1267
+ nestModule: {
1268
+ importFrom: "@icore/db-postgres",
1269
+ symbol: "PostgresDbModule",
1270
+ into: "notes"
1271
+ }
1252
1272
  }
1253
1273
  },
1254
1274
  feature: {
@@ -2238,6 +2258,7 @@ Re-run with @latest to refresh:
2238
2258
  { value: "supabase", label: "Supabase" },
2239
2259
  { value: "firebase", label: "Firebase" },
2240
2260
  { value: "mongodb", label: "MongoDB (Custom Auth)" },
2261
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js + bcrypt + JWT)" },
2241
2262
  { value: "none", label: "None \u2014 no login, open API (simple SPA)" }
2242
2263
  ]
2243
2264
  });
@@ -2247,7 +2268,8 @@ Re-run with @latest to refresh:
2247
2268
  options: [
2248
2269
  { value: "supabase", label: "Supabase Postgres" },
2249
2270
  { value: "firebase", label: "Firestore" },
2250
- { value: "mongodb", label: "MongoDB" }
2271
+ { value: "mongodb", label: "MongoDB" },
2272
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
2251
2273
  ],
2252
2274
  initialValue: authProvider
2253
2275
  });
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- type AuthBackend = 'supabase' | 'firebase' | 'mongodb';
1
+ type AuthBackend = 'supabase' | 'firebase' | 'mongodb' | 'postgres';
2
2
  type AuthProvider = AuthBackend | 'none';
3
- type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'none';
3
+ type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'postgres' | 'none';
4
4
  type UploadProvider = 'supabase' | 'firebase' | 'cloudinary' | 'mongodb' | 'none';
5
5
  type PaymentProvider = 'paypal' | 'none';
6
6
  type JobsProvider = 'bullmq' | 'none';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- type AuthBackend = 'supabase' | 'firebase' | 'mongodb';
1
+ type AuthBackend = 'supabase' | 'firebase' | 'mongodb' | 'postgres';
2
2
  type AuthProvider = AuthBackend | 'none';
3
- type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'none';
3
+ type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'postgres' | 'none';
4
4
  type UploadProvider = 'supabase' | 'firebase' | 'cloudinary' | 'mongodb' | 'none';
5
5
  type PaymentProvider = 'paypal' | 'none';
6
6
  type JobsProvider = 'bullmq' | 'none';
package/dist/index.js CHANGED
@@ -1144,6 +1144,16 @@ var MANIFEST = {
1144
1144
  deps: { mongoose: "^9.6.3" },
1145
1145
  tsPaths: { "@icore/auth-mongodb": ["libs/auth-strategies/mongodb/src/index.ts"] },
1146
1146
  nestModule: { importFrom: "@icore/auth-mongodb", symbol: "MongoDbAuthModule", into: "auth" }
1147
+ },
1148
+ postgres: {
1149
+ libDirs: ["libs/auth-strategies/postgres"],
1150
+ deps: { postgres: "^3", bcrypt: "^6", jsonwebtoken: "^9" },
1151
+ tsPaths: { "@icore/auth-postgres": ["libs/auth-strategies/postgres/src/index.ts"] },
1152
+ nestModule: {
1153
+ importFrom: "@icore/auth-postgres",
1154
+ symbol: "PostgresAuthModule",
1155
+ into: "auth"
1156
+ }
1147
1157
  }
1148
1158
  },
1149
1159
  storage: {
@@ -1206,6 +1216,16 @@ var MANIFEST = {
1206
1216
  deps: { mongoose: "^9.6.3" },
1207
1217
  tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
1208
1218
  nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
1219
+ },
1220
+ postgres: {
1221
+ libDirs: ["libs/db-strategies/postgres"],
1222
+ deps: { postgres: "^3" },
1223
+ tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
1224
+ nestModule: {
1225
+ importFrom: "@icore/db-postgres",
1226
+ symbol: "PostgresDbModule",
1227
+ into: "notes"
1228
+ }
1209
1229
  }
1210
1230
  },
1211
1231
  feature: {
@@ -2195,6 +2215,7 @@ Re-run with @latest to refresh:
2195
2215
  { value: "supabase", label: "Supabase" },
2196
2216
  { value: "firebase", label: "Firebase" },
2197
2217
  { value: "mongodb", label: "MongoDB (Custom Auth)" },
2218
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js + bcrypt + JWT)" },
2198
2219
  { value: "none", label: "None \u2014 no login, open API (simple SPA)" }
2199
2220
  ]
2200
2221
  });
@@ -2204,7 +2225,8 @@ Re-run with @latest to refresh:
2204
2225
  options: [
2205
2226
  { value: "supabase", label: "Supabase Postgres" },
2206
2227
  { value: "firebase", label: "Firestore" },
2207
- { value: "mongodb", label: "MongoDB" }
2228
+ { value: "mongodb", label: "MongoDB" },
2229
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
2208
2230
  ],
2209
2231
  initialValue: authProvider
2210
2232
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idevconn/create-icore",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "iDEVconn",
@@ -1,4 +1,18 @@
1
1
  services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: ${POSTGRES_USER:-icore}
6
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-icore}
7
+ POSTGRES_DB: ${POSTGRES_DB:-icore}
8
+ ports:
9
+ - '5432:5432'
10
+ healthcheck:
11
+ test: ['CMD-SHELL', 'pg_isready -U icore']
12
+ interval: 5s
13
+ retries: 10
14
+ networks: [icore]
15
+
2
16
  redis:
3
17
  image: redis:7-alpine
4
18
  healthcheck:
@@ -0,0 +1,32 @@
1
+ import baseConfig from '../../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
+ '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
+ ],
15
+ ignoredDependencies: [
16
+ '@icore/shared',
17
+ 'bcrypt',
18
+ 'jsonwebtoken',
19
+ 'postgres',
20
+ '@nestjs/common',
21
+ '@nestjs/config',
22
+ '@nestjs/testing',
23
+ 'vitest',
24
+ ],
25
+ },
26
+ ],
27
+ },
28
+ languageOptions: {
29
+ parser: await import('jsonc-eslint-parser'),
30
+ },
31
+ },
32
+ ];
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@icore/auth-postgres",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.ts",
8
+ "dependencies": {
9
+ "@icore/shared": "*",
10
+ "@nestjs/common": "^11.1.27",
11
+ "@nestjs/config": "^4.0.4",
12
+ "bcrypt": "^6.0.0",
13
+ "jsonwebtoken": "^9.0.3",
14
+ "postgres": "^3.4.5",
15
+ "tslib": "^2.8.1"
16
+ },
17
+ "devDependencies": {
18
+ "@nestjs/testing": "^11.1.27",
19
+ "@types/bcrypt": "^6.0.0",
20
+ "@types/jsonwebtoken": "^9.0.10",
21
+ "vitest": "^4.1.9"
22
+ }
23
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "auth-postgres",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/auth-strategies/postgres/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/js:tsc",
10
+ "outputs": ["{options.outputPath}"],
11
+ "options": {
12
+ "outputPath": "dist/libs/auth-strategies/postgres",
13
+ "main": "libs/auth-strategies/postgres/src/index.ts",
14
+ "tsConfig": "libs/auth-strategies/postgres/tsconfig.lib.json",
15
+ "assets": ["libs/auth-strategies/postgres/*.md"]
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ export * from './lib/postgres-auth.strategy';
2
+ export * from './lib/postgres-auth.module';
3
+ export * from './lib/testing/mock-postgres-auth';
@@ -0,0 +1,4 @@
1
+ import { runAuthContract } from '@icore/shared/testing';
2
+ import { createMockPostgresAuth } from '../testing/mock-postgres-auth.js';
3
+
4
+ runAuthContract('PostgresAuthStrategy', () => createMockPostgresAuth());
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Global, Module } from '@nestjs/common';
3
+ import { Test } from '@nestjs/testing';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import { PostgresAuthModule, POSTGRES_AUTH_REQUIRED_ENV } from '../postgres-auth.module.js';
6
+ import { PostgresAuthStrategy } from '../postgres-auth.strategy.js';
7
+
8
+ let ENV: Record<string, string | undefined> = {};
9
+
10
+ @Global()
11
+ @Module({
12
+ providers: [
13
+ {
14
+ provide: ConfigService,
15
+ useValue: {
16
+ get: (k: string) => ENV[k],
17
+ getOrThrow: (k: string) => ENV[k],
18
+ },
19
+ },
20
+ ],
21
+ exports: [ConfigService],
22
+ })
23
+ class StubConfigModule {}
24
+
25
+ describe('PostgresAuthModule', () => {
26
+ it('declares its required env', () => {
27
+ expect(POSTGRES_AUTH_REQUIRED_ENV).toEqual(['POSTGRES_URL', 'JWT_SECRET']);
28
+ });
29
+
30
+ it('provides a real PostgresAuthStrategy under AuthStrategy when env present', async () => {
31
+ ENV = {
32
+ POSTGRES_URL: 'postgresql://user:pass@localhost:5432/test',
33
+ JWT_SECRET: 'test-secret',
34
+ };
35
+ const ref = await Test.createTestingModule({
36
+ imports: [StubConfigModule, PostgresAuthModule.forRoot('.env')],
37
+ }).compile();
38
+ expect(ref.get('AuthStrategy')).toBeInstanceOf(PostgresAuthStrategy);
39
+ });
40
+
41
+ it('provides FakeAuthStrategy when JWT_SECRET is missing', async () => {
42
+ ENV = { POSTGRES_URL: 'postgresql://user:pass@localhost:5432/test' };
43
+ const ref = await Test.createTestingModule({
44
+ imports: [StubConfigModule, PostgresAuthModule.forRoot('.env')],
45
+ }).compile();
46
+ const strategy = ref.get('AuthStrategy');
47
+ expect(strategy).not.toBeInstanceOf(PostgresAuthStrategy);
48
+ });
49
+ });
@@ -0,0 +1,39 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { buildStrategyWithFallback, FakeAuthStrategy } from '@icore/shared';
4
+ import type { AuthStrategy } from '@icore/shared';
5
+ import { PostgresAuthStrategy } from './postgres-auth.strategy';
6
+
7
+ export const POSTGRES_AUTH_REQUIRED_ENV = ['POSTGRES_URL', 'JWT_SECRET'];
8
+
9
+ @Module({})
10
+ export class PostgresAuthModule {
11
+ static forRoot(envPath: string): DynamicModule {
12
+ return {
13
+ module: PostgresAuthModule,
14
+ providers: [
15
+ {
16
+ provide: 'AuthStrategy',
17
+ useFactory: (cfg: ConfigService): AuthStrategy =>
18
+ buildStrategyWithFallback<AuthStrategy>({
19
+ service: 'auth MS',
20
+ provider: 'postgres',
21
+ requiredEnv: POSTGRES_AUTH_REQUIRED_ENV,
22
+ cfg,
23
+ envPath,
24
+ build: () =>
25
+ new PostgresAuthStrategy({
26
+ url: cfg.getOrThrow<string>('POSTGRES_URL'),
27
+ jwtSecret: cfg.getOrThrow<string>('JWT_SECRET'),
28
+ jwtExpiresIn: cfg.get<string>('JWT_EXPIRES_IN'),
29
+ refreshExpiresIn: cfg.get<string>('JWT_REFRESH_EXPIRES_IN'),
30
+ }),
31
+ fake: () => new FakeAuthStrategy(),
32
+ }),
33
+ inject: [ConfigService],
34
+ },
35
+ ],
36
+ exports: ['AuthStrategy'],
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,210 @@
1
+ import postgres from 'postgres';
2
+ import * as bcrypt from 'bcrypt';
3
+ import * as jwt from 'jsonwebtoken';
4
+ import { randomUUID } from 'node:crypto';
5
+ import type {
6
+ AuthSession,
7
+ AuthStrategy,
8
+ MagicLinkRequest,
9
+ OAuthProvider,
10
+ OAuthStartResult,
11
+ VerifiedToken,
12
+ } from '@icore/shared';
13
+
14
+ export interface PostgresAuthStrategyOptions {
15
+ url: string;
16
+ jwtSecret: string;
17
+ jwtExpiresIn?: string;
18
+ refreshExpiresIn?: string;
19
+ }
20
+
21
+ function parseDurationSeconds(s: string): number {
22
+ const m = /^(\d+)(s|m|h|d)$/.exec(s);
23
+ if (!m) return 900;
24
+ const n = parseInt(m[1] as string, 10);
25
+ const unit = m[2] as string;
26
+ if (unit === 's') return n;
27
+ if (unit === 'm') return n * 60;
28
+ if (unit === 'h') return n * 3600;
29
+ return n * 86400;
30
+ }
31
+
32
+ function parseDurationMs(s: string): number {
33
+ return parseDurationSeconds(s) * 1000;
34
+ }
35
+
36
+ export class PostgresAuthStrategy implements AuthStrategy {
37
+ private readonly sql: postgres.Sql;
38
+ private tablesReadyPromise: Promise<void> | null = null;
39
+
40
+ constructor(private readonly opts: PostgresAuthStrategyOptions) {
41
+ this.sql = postgres(opts.url);
42
+ }
43
+
44
+ private ensureTables(): Promise<void> {
45
+ if (!this.tablesReadyPromise) {
46
+ this.tablesReadyPromise = this._createTables();
47
+ }
48
+ return this.tablesReadyPromise;
49
+ }
50
+
51
+ private async _createTables(): Promise<void> {
52
+ await this.sql`
53
+ CREATE TABLE IF NOT EXISTS _icore_users (
54
+ id TEXT PRIMARY KEY,
55
+ email TEXT UNIQUE NOT NULL,
56
+ password_hash TEXT,
57
+ role TEXT,
58
+ last_logged_in TIMESTAMPTZ,
59
+ created_at TIMESTAMPTZ DEFAULT now()
60
+ )
61
+ `;
62
+ await this.sql`
63
+ CREATE TABLE IF NOT EXISTS _icore_sessions (
64
+ id TEXT PRIMARY KEY,
65
+ user_id TEXT NOT NULL,
66
+ refresh_token TEXT UNIQUE NOT NULL,
67
+ expires_at TIMESTAMPTZ NOT NULL
68
+ )
69
+ `;
70
+ }
71
+
72
+ async verifyToken(token: string): Promise<VerifiedToken> {
73
+ try {
74
+ const decoded = jwt.verify(token, this.opts.jwtSecret) as jwt.JwtPayload;
75
+ return {
76
+ uid: decoded.sub as string,
77
+ email: decoded['email'] as string,
78
+ role: decoded['role'] as string,
79
+ };
80
+ } catch (err) {
81
+ throw new Error('invalid_token', { cause: err });
82
+ }
83
+ }
84
+
85
+ async signIn(email: string, password: string): Promise<AuthSession> {
86
+ await this.ensureTables();
87
+ const rows = await this.sql<
88
+ { id: string; email: string; password_hash: string; role: string | null }[]
89
+ >`
90
+ SELECT id, email, password_hash, role FROM _icore_users WHERE email = ${email}
91
+ `;
92
+ const user = rows[0];
93
+ if (!user || !user.password_hash) throw new Error('invalid_credentials');
94
+ const ok = await bcrypt.compare(password, user.password_hash);
95
+ if (!ok) throw new Error('invalid_credentials');
96
+ await this.sql`
97
+ UPDATE _icore_users SET last_logged_in = now() WHERE id = ${user.id}
98
+ `;
99
+ return this.createSession({ id: user.id, email: user.email, role: user.role ?? undefined });
100
+ }
101
+
102
+ async signUp(email: string, password: string): Promise<AuthSession> {
103
+ await this.ensureTables();
104
+ const id = randomUUID();
105
+ const passwordHash = await bcrypt.hash(password, 10);
106
+ try {
107
+ await this.sql`
108
+ INSERT INTO _icore_users (id, email, password_hash) VALUES (${id}, ${email}, ${passwordHash})
109
+ `;
110
+ } catch (err: unknown) {
111
+ if (
112
+ err &&
113
+ typeof err === 'object' &&
114
+ 'code' in err &&
115
+ (err as { code: string }).code === '23505'
116
+ ) {
117
+ throw new Error('user_already_exists', { cause: err });
118
+ }
119
+ throw err;
120
+ }
121
+ return this.createSession({ id, email });
122
+ }
123
+
124
+ async refresh(refreshToken: string): Promise<AuthSession> {
125
+ await this.ensureTables();
126
+ const sessions = await this.sql<{ id: string; user_id: string; expires_at: Date }[]>`
127
+ SELECT id, user_id, expires_at FROM _icore_sessions WHERE refresh_token = ${refreshToken}
128
+ `;
129
+ const session = sessions[0];
130
+ if (!session || session.expires_at < new Date()) {
131
+ if (session) {
132
+ await this.sql`DELETE FROM _icore_sessions WHERE id = ${session.id}`;
133
+ }
134
+ throw new Error('invalid_refresh_token');
135
+ }
136
+ const users = await this.sql<{ id: string; email: string; role: string | null }[]>`
137
+ SELECT id, email, role FROM _icore_users WHERE id = ${session.user_id}
138
+ `;
139
+ const user = users[0];
140
+ if (!user) throw new Error('user_not_found');
141
+ await this.sql`DELETE FROM _icore_sessions WHERE id = ${session.id}`;
142
+ await this.sql`
143
+ UPDATE _icore_users SET last_logged_in = now() WHERE id = ${user.id}
144
+ `;
145
+ return this.createSession({ id: user.id, email: user.email, role: user.role ?? undefined });
146
+ }
147
+
148
+ async setRole(uid: string, role: string): Promise<void> {
149
+ await this.ensureTables();
150
+ await this.sql`UPDATE _icore_users SET role = ${role} WHERE id = ${uid}`;
151
+ }
152
+
153
+ async getRole(uid: string): Promise<string | null> {
154
+ await this.ensureTables();
155
+ const rows = await this.sql<{ role: string | null }[]>`
156
+ SELECT role FROM _icore_users WHERE id = ${uid}
157
+ `;
158
+ return rows[0]?.role ?? null;
159
+ }
160
+
161
+ async sendMagicLink(_req: MagicLinkRequest): Promise<void> {
162
+ throw new Error('not_implemented');
163
+ }
164
+
165
+ async verifyMagicLink(_token: string): Promise<AuthSession> {
166
+ throw new Error('not_implemented');
167
+ }
168
+
169
+ async startOAuth(_provider: OAuthProvider, _callbackUrl: string): Promise<OAuthStartResult> {
170
+ throw new Error('not_implemented');
171
+ }
172
+
173
+ async completeOAuth(
174
+ _provider: OAuthProvider,
175
+ _code: string,
176
+ _state: string,
177
+ ): Promise<AuthSession> {
178
+ throw new Error('not_implemented');
179
+ }
180
+
181
+ private async createSession(user: {
182
+ id: string;
183
+ email: string;
184
+ role?: string;
185
+ }): Promise<AuthSession> {
186
+ const expiresIn = this.opts.jwtExpiresIn ?? '15m';
187
+ const accessToken = jwt.sign(
188
+ { sub: user.id, email: user.email, role: user.role },
189
+ this.opts.jwtSecret,
190
+ { expiresIn: expiresIn as jwt.SignOptions['expiresIn'] },
191
+ );
192
+ const refreshToken = randomUUID();
193
+ const refreshMs = parseDurationMs(this.opts.refreshExpiresIn ?? '7d');
194
+ const expiresAt = new Date(Date.now() + refreshMs);
195
+ await this.sql`
196
+ INSERT INTO _icore_sessions (id, user_id, refresh_token, expires_at)
197
+ VALUES (${randomUUID()}, ${user.id}, ${refreshToken}, ${expiresAt})
198
+ `;
199
+ return {
200
+ accessToken,
201
+ refreshToken,
202
+ expiresIn: parseDurationSeconds(expiresIn),
203
+ user: { id: user.id, email: user.email },
204
+ };
205
+ }
206
+
207
+ async end(): Promise<void> {
208
+ await this.sql.end();
209
+ }
210
+ }