@syncello/auth 2.5.1

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.
@@ -0,0 +1,1100 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/init.ts
7
+ import * as fs3 from "fs";
8
+ import * as path2 from "path";
9
+
10
+ // src/schema/definitions.ts
11
+ var SCHEMA_VERSION = "2026.05.1";
12
+ var enums = {
13
+ identifierType: {
14
+ name: "identifier_type_enum",
15
+ values: ["email", "ip"]
16
+ },
17
+ twoFactorMethod: {
18
+ name: "two_factor_method",
19
+ values: ["totp", "email"]
20
+ },
21
+ deviceType: {
22
+ name: "device_type",
23
+ values: ["desktop", "mobile", "tablet"]
24
+ }
25
+ };
26
+ var tables = {
27
+ users: {
28
+ columns: {
29
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
30
+ email: { type: "varchar", length: 255, notNull: true, unique: true },
31
+ name: { type: "text" },
32
+ hashedPassword: { type: "varchar", length: 255 },
33
+ pepperKid: { type: "varchar", length: 10, default: "v1" },
34
+ emailVerified: { type: "boolean", default: false },
35
+ isAdmin: { type: "boolean", default: false, notNull: true },
36
+ sessionVersion: { type: "integer", default: 1 },
37
+ lockedUntil: { type: "bigint", mode: "number" },
38
+ failedLoginCount: { type: "integer", default: 0 },
39
+ emailBounced: { type: "boolean", default: false, notNull: true },
40
+ emailBouncedAt: { type: "timestamp", withTimezone: true },
41
+ emailComplained: { type: "boolean", default: false, notNull: true },
42
+ emailComplainedAt: { type: "timestamp", withTimezone: true },
43
+ activatedAt: { type: "timestamp", withTimezone: true },
44
+ deletedAt: { type: "timestamp", withTimezone: true },
45
+ scheduledPurgeAt: { type: "timestamp", withTimezone: true },
46
+ createdAt: { type: "timestamp", withTimezone: true, default: "now()" },
47
+ updatedAt: { type: "timestamp", withTimezone: true, default: "now()" }
48
+ },
49
+ indexes: [{ name: "idx_users_email", columns: ["email"] }]
50
+ },
51
+ sessions: {
52
+ columns: {
53
+ id: { type: "varchar", length: 128, primaryKey: true },
54
+ userId: {
55
+ type: "uuid",
56
+ notNull: true,
57
+ references: { table: "users", column: "id", onDelete: "cascade" }
58
+ },
59
+ expiresAt: { type: "bigint", mode: "number", notNull: true },
60
+ createdAt: { type: "bigint", mode: "number", notNull: true },
61
+ lastActiveAt: { type: "bigint", mode: "number" },
62
+ fingerprint: { type: "varchar", length: 64 },
63
+ ipAddress: { type: "varchar", length: 45 }
64
+ },
65
+ indexes: [
66
+ { name: "idx_sessions_expires", columns: ["expiresAt"] },
67
+ { name: "idx_sessions_user", columns: ["userId"] }
68
+ ]
69
+ },
70
+ user2faMethods: {
71
+ columns: {
72
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
73
+ userId: {
74
+ type: "uuid",
75
+ notNull: true,
76
+ references: { table: "users", column: "id", onDelete: "cascade" }
77
+ },
78
+ method: { type: "two_factor_method", notNull: true },
79
+ totpSecret: { type: "varchar", length: 256 },
80
+ lastTotpCounter: { type: "bigint", mode: "number" },
81
+ isPrimary: { type: "boolean", default: false, notNull: true },
82
+ verifiedAt: { type: "bigint", mode: "number", notNull: true },
83
+ createdAt: { type: "bigint", mode: "number", notNull: true }
84
+ },
85
+ indexes: [
86
+ { name: "user_2fa_methods_user_method_idx", columns: ["userId", "method"], unique: true },
87
+ { name: "user_2fa_methods_user_idx", columns: ["userId"] }
88
+ ]
89
+ },
90
+ userBackupCodes: {
91
+ columns: {
92
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
93
+ userId: {
94
+ type: "uuid",
95
+ notNull: true,
96
+ references: { table: "users", column: "id", onDelete: "cascade" }
97
+ },
98
+ codeHash: { type: "varchar", length: 255, notNull: true },
99
+ usedAt: { type: "bigint", mode: "number" },
100
+ createdAt: { type: "bigint", mode: "number", notNull: true }
101
+ },
102
+ indexes: [{ name: "user_backup_codes_user_idx", columns: ["userId"] }]
103
+ },
104
+ userTrustedDevices: {
105
+ columns: {
106
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
107
+ userId: {
108
+ type: "uuid",
109
+ notNull: true,
110
+ references: { table: "users", column: "id", onDelete: "cascade" }
111
+ },
112
+ tokenHash: { type: "varchar", length: 128, notNull: true },
113
+ deviceName: { type: "varchar", length: 255, notNull: true },
114
+ deviceType: { type: "device_type", notNull: true, default: "desktop" },
115
+ ipAddress: { type: "varchar", length: 45, notNull: true },
116
+ expiresAt: { type: "bigint", mode: "number", notNull: true },
117
+ lastUsedAt: { type: "bigint", mode: "number", notNull: true },
118
+ createdAt: { type: "bigint", mode: "number", notNull: true }
119
+ },
120
+ indexes: [
121
+ { name: "user_trusted_devices_user_idx", columns: ["userId"] },
122
+ { name: "user_trusted_devices_token_idx", columns: ["tokenHash"] },
123
+ { name: "user_trusted_devices_expires_idx", columns: ["expiresAt"] }
124
+ ]
125
+ },
126
+ emailVerificationTokens: {
127
+ columns: {
128
+ tokenHash: { type: "varchar", length: 128, primaryKey: true },
129
+ userId: {
130
+ type: "uuid",
131
+ notNull: true,
132
+ references: { table: "users", column: "id", onDelete: "cascade" }
133
+ },
134
+ email: { type: "varchar", length: 255, notNull: true },
135
+ expiresAt: { type: "bigint", mode: "number", notNull: true },
136
+ createdAt: { type: "bigint", mode: "number", notNull: true }
137
+ },
138
+ indexes: [
139
+ { name: "idx_email_verification_user", columns: ["userId"] },
140
+ { name: "idx_email_verification_expires", columns: ["expiresAt"] }
141
+ ]
142
+ },
143
+ passwordResetTokens: {
144
+ columns: {
145
+ tokenHash: { type: "varchar", length: 128, primaryKey: true },
146
+ userId: {
147
+ type: "uuid",
148
+ notNull: true,
149
+ references: { table: "users", column: "id", onDelete: "cascade" }
150
+ },
151
+ expiresAt: { type: "bigint", mode: "number", notNull: true },
152
+ used: { type: "boolean", default: false },
153
+ createdAt: { type: "bigint", mode: "number", notNull: true }
154
+ },
155
+ indexes: [
156
+ { name: "idx_password_reset_user", columns: ["userId"] },
157
+ { name: "idx_password_reset_expires", columns: ["expiresAt"] }
158
+ ]
159
+ },
160
+ emailChangeTokens: {
161
+ columns: {
162
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
163
+ userId: {
164
+ type: "uuid",
165
+ notNull: true,
166
+ references: { table: "users", column: "id", onDelete: "cascade" }
167
+ },
168
+ newEmail: { type: "varchar", length: 255, notNull: true },
169
+ tokenHash: { type: "varchar", length: 128, notNull: true },
170
+ cancelTokenHash: { type: "varchar", length: 128, notNull: true },
171
+ expiresAt: { type: "bigint", mode: "number", notNull: true },
172
+ createdAt: { type: "bigint", mode: "number", notNull: true }
173
+ },
174
+ indexes: [
175
+ { name: "idx_email_change_tokens_user", columns: ["userId"], unique: true },
176
+ { name: "idx_email_change_tokens_token", columns: ["tokenHash"] },
177
+ { name: "idx_email_change_tokens_cancel", columns: ["cancelTokenHash"] },
178
+ { name: "idx_email_change_tokens_expires", columns: ["expiresAt"] }
179
+ ]
180
+ },
181
+ emailEvents: {
182
+ columns: {
183
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
184
+ emailId: { type: "text", notNull: true },
185
+ userId: {
186
+ type: "uuid",
187
+ references: { table: "users", column: "id", onDelete: "cascade" }
188
+ },
189
+ eventType: { type: "text", notNull: true },
190
+ emailAddress: { type: "text", notNull: true },
191
+ metadata: { type: "json" },
192
+ createdAt: { type: "timestamp", withTimezone: true, default: "now()", notNull: true }
193
+ },
194
+ indexes: [
195
+ { name: "email_events_email_address_idx", columns: ["emailAddress"] },
196
+ { name: "email_events_user_id_idx", columns: ["userId"] },
197
+ { name: "email_events_email_id_event_type_idx", columns: ["emailId", "eventType"], unique: true }
198
+ ]
199
+ },
200
+ failedLoginAttempts: {
201
+ columns: {
202
+ id: { type: "serial", primaryKey: true },
203
+ identifier: { type: "varchar", length: 255, notNull: true },
204
+ identifierType: { type: "identifier_type_enum", notNull: true },
205
+ attemptedAt: { type: "bigint", mode: "number", notNull: true }
206
+ },
207
+ indexes: [
208
+ { name: "idx_failed_attempts_identifier", columns: ["identifier", "attemptedAt"] },
209
+ { name: "idx_failed_attempts_time", columns: ["attemptedAt"] }
210
+ ]
211
+ },
212
+ securityAuditLog: {
213
+ columns: {
214
+ id: { type: "serial", primaryKey: true },
215
+ userId: {
216
+ type: "uuid",
217
+ references: { table: "users", column: "id", onDelete: "set null" }
218
+ },
219
+ eventType: { type: "varchar", length: 50, notNull: true },
220
+ eventData: { type: "json" },
221
+ ipAddress: { type: "varchar", length: 45 },
222
+ userAgent: { type: "text" },
223
+ success: { type: "boolean", notNull: true },
224
+ createdAt: { type: "timestamp", withTimezone: true, default: "now()" }
225
+ },
226
+ indexes: [
227
+ { name: "idx_audit_user", columns: ["userId"] },
228
+ { name: "idx_audit_event", columns: ["eventType"] },
229
+ { name: "idx_audit_created", columns: ["createdAt"] }
230
+ ]
231
+ },
232
+ oauthAccounts: {
233
+ columns: {
234
+ id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
235
+ userId: {
236
+ type: "uuid",
237
+ notNull: true,
238
+ references: { table: "users", column: "id", onDelete: "cascade" }
239
+ },
240
+ provider: { type: "varchar", length: 50, notNull: true },
241
+ providerUserId: { type: "varchar", length: 255, notNull: true },
242
+ email: { type: "varchar", length: 255, notNull: true },
243
+ createdAt: { type: "timestamp", withTimezone: true, default: "now()", notNull: true }
244
+ },
245
+ indexes: [
246
+ { name: "oauth_accounts_user_provider_idx", columns: ["userId", "provider"], unique: true },
247
+ { name: "oauth_accounts_provider_user_idx", columns: ["provider", "providerUserId"], unique: true }
248
+ ]
249
+ }
250
+ };
251
+ var tableNames = {
252
+ users: "users",
253
+ sessions: "sessions",
254
+ user2faMethods: "user_2fa_methods",
255
+ userBackupCodes: "user_backup_codes",
256
+ userTrustedDevices: "user_trusted_devices",
257
+ emailVerificationTokens: "email_verification_tokens",
258
+ passwordResetTokens: "password_reset_tokens",
259
+ emailChangeTokens: "email_change_tokens",
260
+ emailEvents: "email_events",
261
+ failedLoginAttempts: "failed_login_attempts",
262
+ securityAuditLog: "security_audit_log",
263
+ oauthAccounts: "oauth_accounts"
264
+ };
265
+
266
+ // src/schema/generator.ts
267
+ function toSnakeCase(str) {
268
+ return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
269
+ }
270
+ function toPascalCase(str) {
271
+ return str.split(/[_-]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
272
+ }
273
+ function isEnumType(type) {
274
+ return Object.values(enums).some((e) => e.name === type);
275
+ }
276
+ function getEnumVarName(enumDbName) {
277
+ const entry = Object.entries(enums).find(([_, e]) => e.name === enumDbName);
278
+ return entry ? `${entry[0]}Enum` : enumDbName;
279
+ }
280
+ function generateColumnCode(name, col) {
281
+ const snakeName = toSnakeCase(name);
282
+ const parts = [];
283
+ if (isEnumType(col.type)) {
284
+ parts.push(`${getEnumVarName(col.type)}('${snakeName}')`);
285
+ } else {
286
+ switch (col.type) {
287
+ case "uuid":
288
+ parts.push(`uuid('${snakeName}')`);
289
+ break;
290
+ case "varchar":
291
+ parts.push(`varchar('${snakeName}', { length: ${col.length ?? 255} })`);
292
+ break;
293
+ case "text":
294
+ parts.push(`text('${snakeName}')`);
295
+ break;
296
+ case "boolean":
297
+ parts.push(`boolean('${snakeName}')`);
298
+ break;
299
+ case "integer":
300
+ parts.push(`integer('${snakeName}')`);
301
+ break;
302
+ case "bigint":
303
+ parts.push(`bigint('${snakeName}', { mode: 'number' })`);
304
+ break;
305
+ case "timestamp":
306
+ if (col.withTimezone) {
307
+ parts.push(`timestamp('${snakeName}', { withTimezone: true })`);
308
+ } else {
309
+ parts.push(`timestamp('${snakeName}')`);
310
+ }
311
+ break;
312
+ case "json":
313
+ parts.push(`json('${snakeName}')`);
314
+ break;
315
+ case "serial":
316
+ parts.push(`serial('${snakeName}')`);
317
+ break;
318
+ default:
319
+ parts.push(`text('${snakeName}')`);
320
+ }
321
+ }
322
+ if (col.primaryKey) {
323
+ parts.push(".primaryKey()");
324
+ }
325
+ if (col.notNull && !col.primaryKey) {
326
+ parts.push(".notNull()");
327
+ }
328
+ if (col.unique) {
329
+ parts.push(".unique()");
330
+ }
331
+ if (col.default !== void 0) {
332
+ if (col.default === "gen_random_uuid()") {
333
+ parts.push(".defaultRandom()");
334
+ } else if (col.default === "now()") {
335
+ parts.push(".defaultNow()");
336
+ } else if (typeof col.default === "boolean") {
337
+ parts.push(`.default(${col.default})`);
338
+ } else if (typeof col.default === "number") {
339
+ parts.push(`.default(${col.default})`);
340
+ } else if (typeof col.default === "string") {
341
+ parts.push(`.default('${col.default}')`);
342
+ }
343
+ }
344
+ if (col.references) {
345
+ const refTable = col.references.table;
346
+ const refColumn = col.references.column;
347
+ const onDelete = col.references.onDelete ?? "no action";
348
+ parts.push(`.references(() => ${refTable}.${refColumn}, { onDelete: '${onDelete}' })`);
349
+ }
350
+ return parts.join("");
351
+ }
352
+ function generateIndexCode(idx, tableVar) {
353
+ const columns = idx.columns.map((c) => `${tableVar}.${c}`).join(", ");
354
+ if (idx.unique) {
355
+ return `uniqueIndex('${idx.name}').on(${columns})`;
356
+ }
357
+ return `index('${idx.name}').on(${columns})`;
358
+ }
359
+ function deriveRelationName(columnName) {
360
+ if (columnName.endsWith("Id")) {
361
+ return columnName.slice(0, -2);
362
+ }
363
+ return columnName;
364
+ }
365
+ function pluralize(name) {
366
+ if (name.endsWith("s")) return name;
367
+ if (name.endsWith("y") && !name.endsWith("ey") && !name.endsWith("ay") && !name.endsWith("oy")) {
368
+ return name.slice(0, -1) + "ies";
369
+ }
370
+ return name + "s";
371
+ }
372
+ function extractForeignKeys() {
373
+ const foreignKeys = [];
374
+ for (const [tableName, tableDef] of Object.entries(tables)) {
375
+ for (const [columnName, columnDef] of Object.entries(tableDef.columns)) {
376
+ const col = columnDef;
377
+ if (col.references) {
378
+ foreignKeys.push({
379
+ tableName,
380
+ columnName,
381
+ referencedTable: col.references.table,
382
+ referencedColumn: col.references.column
383
+ });
384
+ }
385
+ }
386
+ }
387
+ return foreignKeys;
388
+ }
389
+ function buildRelations(foreignKeys) {
390
+ const relationsByTable = /* @__PURE__ */ new Map();
391
+ const addRelation = (tableName, relation) => {
392
+ if (!relationsByTable.has(tableName)) {
393
+ relationsByTable.set(tableName, []);
394
+ }
395
+ relationsByTable.get(tableName).push(relation);
396
+ };
397
+ for (const fk of foreignKeys) {
398
+ const relationName = deriveRelationName(fk.columnName);
399
+ addRelation(fk.tableName, {
400
+ tableName: fk.tableName,
401
+ relationName,
402
+ type: "one",
403
+ targetTable: fk.referencedTable,
404
+ fieldColumn: fk.columnName,
405
+ refColumn: fk.referencedColumn
406
+ });
407
+ const manyRelationName = pluralize(fk.tableName);
408
+ addRelation(fk.referencedTable, {
409
+ tableName: fk.referencedTable,
410
+ relationName: manyRelationName,
411
+ type: "many",
412
+ targetTable: fk.tableName
413
+ });
414
+ }
415
+ return relationsByTable;
416
+ }
417
+ function generateRelations() {
418
+ const foreignKeys = extractForeignKeys();
419
+ const relationsByTable = buildRelations(foreignKeys);
420
+ const lines = [];
421
+ const sortedTables = Array.from(relationsByTable.keys()).sort();
422
+ for (const tableName of sortedTables) {
423
+ const relations = relationsByTable.get(tableName);
424
+ const oneRelations = relations.filter((r) => r.type === "one");
425
+ const manyRelations = relations.filter((r) => r.type === "many");
426
+ const params = [];
427
+ if (oneRelations.length > 0) params.push("one");
428
+ if (manyRelations.length > 0) params.push("many");
429
+ if (params.length === 0) continue;
430
+ lines.push(`export const ${tableName}Relations = relations(${tableName}, ({ ${params.join(", ")} }) => ({`);
431
+ for (const rel of oneRelations) {
432
+ lines.push(` ${rel.relationName}: one(${rel.targetTable}, {`);
433
+ lines.push(` fields: [${rel.tableName}.${rel.fieldColumn}],`);
434
+ lines.push(` references: [${rel.targetTable}.${rel.refColumn}],`);
435
+ lines.push(" }),");
436
+ }
437
+ for (const rel of manyRelations) {
438
+ lines.push(` ${rel.relationName}: many(${rel.targetTable}),`);
439
+ }
440
+ lines.push("}));");
441
+ lines.push("");
442
+ }
443
+ return lines.join("\n");
444
+ }
445
+ function generateSchemaFile() {
446
+ const lines = [];
447
+ lines.push("/**");
448
+ lines.push(" * AUTO-GENERATED by @syncello/auth - DO NOT EDIT");
449
+ lines.push(` * Schema version: ${SCHEMA_VERSION}`);
450
+ lines.push(" * Regenerate with: npx syncello-auth sync");
451
+ lines.push(" */");
452
+ lines.push("");
453
+ lines.push("import {");
454
+ lines.push(" pgTable,");
455
+ lines.push(" pgEnum,");
456
+ lines.push(" uuid,");
457
+ lines.push(" varchar,");
458
+ lines.push(" text,");
459
+ lines.push(" boolean,");
460
+ lines.push(" integer,");
461
+ lines.push(" bigint,");
462
+ lines.push(" timestamp,");
463
+ lines.push(" json,");
464
+ lines.push(" serial,");
465
+ lines.push(" index,");
466
+ lines.push(" uniqueIndex,");
467
+ lines.push("} from 'drizzle-orm/pg-core';");
468
+ lines.push("import { relations } from 'drizzle-orm';");
469
+ lines.push("");
470
+ lines.push("// ====================================");
471
+ lines.push("// ENUMS");
472
+ lines.push("// ====================================");
473
+ lines.push("");
474
+ for (const [key, enumDef] of Object.entries(enums)) {
475
+ const varName = `${key}Enum`;
476
+ const values = enumDef.values.map((v) => `'${v}'`).join(", ");
477
+ lines.push(`export const ${varName} = pgEnum('${enumDef.name}', [${values}]);`);
478
+ }
479
+ lines.push("");
480
+ lines.push("// ====================================");
481
+ lines.push("// TABLES");
482
+ lines.push("// ====================================");
483
+ lines.push("");
484
+ for (const [tableKey, tableDef] of Object.entries(tables)) {
485
+ const dbTableName = tableNames[tableKey];
486
+ lines.push(`export const ${tableKey} = pgTable('${dbTableName}', {`);
487
+ for (const [colName, colDef] of Object.entries(tableDef.columns)) {
488
+ const colCode = generateColumnCode(colName, colDef);
489
+ lines.push(` ${colName}: ${colCode},`);
490
+ }
491
+ if (tableDef.indexes && tableDef.indexes.length > 0) {
492
+ lines.push("}, (table) => ({");
493
+ for (const idx of tableDef.indexes) {
494
+ const idxCode = generateIndexCode(idx, "table");
495
+ const idxVarName = idx.name.replace(/-/g, "_");
496
+ lines.push(` ${idxVarName}: ${idxCode},`);
497
+ }
498
+ lines.push("}));");
499
+ } else {
500
+ lines.push("});");
501
+ }
502
+ lines.push("");
503
+ }
504
+ lines.push("// ====================================");
505
+ lines.push("// RELATIONS");
506
+ lines.push("// ====================================");
507
+ lines.push("");
508
+ lines.push(generateRelations());
509
+ lines.push("// ====================================");
510
+ lines.push("// TYPE EXPORTS");
511
+ lines.push("// ====================================");
512
+ lines.push("");
513
+ for (const tableKey of Object.keys(tables)) {
514
+ const typeName = toPascalCase(tableKey);
515
+ lines.push(`export type ${typeName} = typeof ${tableKey}.$inferSelect;`);
516
+ lines.push(`export type New${typeName} = typeof ${tableKey}.$inferInsert;`);
517
+ }
518
+ return lines.join("\n");
519
+ }
520
+ function generateExtensionsFile() {
521
+ const lines = [];
522
+ lines.push("/**");
523
+ lines.push(" * Client extensions for auth schema - this file is PRESERVED during sync");
524
+ lines.push(" * Add your custom columns here");
525
+ lines.push(" *");
526
+ lines.push(" * Example:");
527
+ lines.push(" * export const userExtensions = {");
528
+ lines.push(" * avatarUrl: varchar('avatar_url', { length: 500 }),");
529
+ lines.push(" * stripeCustomerId: varchar('stripe_customer_id', { length: 255 }),");
530
+ lines.push(" * };");
531
+ lines.push(" */");
532
+ lines.push("");
533
+ lines.push("import type { PgColumnBuilderBase } from 'drizzle-orm/pg-core';");
534
+ lines.push("");
535
+ lines.push("export const userExtensions: Record<string, PgColumnBuilderBase> = {};");
536
+ lines.push("");
537
+ lines.push("export const sessionExtensions: Record<string, PgColumnBuilderBase> = {};");
538
+ lines.push("");
539
+ return lines.join("\n");
540
+ }
541
+ function generateIndexFile() {
542
+ const lines = [];
543
+ lines.push("/**");
544
+ lines.push(" * Schema Index");
545
+ lines.push(" * Exports all auth tables");
546
+ lines.push(" *");
547
+ lines.push(" * To add custom columns to auth tables, edit auth-extensions.ts");
548
+ lines.push(" * Those extensions will be merged during schema generation.");
549
+ lines.push(" */");
550
+ lines.push("");
551
+ lines.push("export * from './auth';");
552
+ lines.push("");
553
+ lines.push("// Re-export extension types if needed");
554
+ lines.push("export * from './auth-extensions';");
555
+ lines.push("");
556
+ return lines.join("\n");
557
+ }
558
+
559
+ // src/cli/utils/config.ts
560
+ import * as fs from "fs";
561
+ import * as path from "path";
562
+ var CONFIG_FILENAME = "syncello-auth.json";
563
+ function getConfigPath(cwd = process.cwd()) {
564
+ return path.join(cwd, CONFIG_FILENAME);
565
+ }
566
+ function configExists(cwd = process.cwd()) {
567
+ return fs.existsSync(getConfigPath(cwd));
568
+ }
569
+ function loadConfig(cwd = process.cwd()) {
570
+ const configPath = getConfigPath(cwd);
571
+ if (!fs.existsSync(configPath)) {
572
+ return null;
573
+ }
574
+ const content = fs.readFileSync(configPath, "utf-8");
575
+ return JSON.parse(content);
576
+ }
577
+ function saveConfig(config, cwd = process.cwd()) {
578
+ const configPath = getConfigPath(cwd);
579
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
580
+ }
581
+ function createInitialConfig(libraryVersion, schemaHash) {
582
+ return {
583
+ libraryVersion,
584
+ schemaVersion: SCHEMA_VERSION,
585
+ schemaHash,
586
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
587
+ extensions: {
588
+ enabled: true,
589
+ file: "db/schema/auth-extensions.ts"
590
+ }
591
+ };
592
+ }
593
+
594
+ // src/cli/utils/hash.ts
595
+ import * as crypto from "crypto";
596
+ import * as fs2 from "fs";
597
+ function hashContent(content) {
598
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
599
+ }
600
+ function hashFile(filePath) {
601
+ if (!fs2.existsSync(filePath)) {
602
+ return null;
603
+ }
604
+ const content = fs2.readFileSync(filePath, "utf-8");
605
+ return hashContent(content);
606
+ }
607
+
608
+ // src/cli/commands/init.ts
609
+ function getLibraryVersion() {
610
+ try {
611
+ const pkgPath = path2.resolve(__dirname, "../../../package.json");
612
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
613
+ return pkg.version;
614
+ } catch {
615
+ return "0.0.0";
616
+ }
617
+ }
618
+ async function init(options) {
619
+ const cwd = process.cwd();
620
+ const outputDir = options.output || "db/schema";
621
+ const schemaDir = path2.join(cwd, outputDir);
622
+ if (configExists(cwd) && !options.force) {
623
+ console.log("\u274C @syncello/auth is already initialized in this project.");
624
+ console.log(" Use --force to reinitialize (this will overwrite auth.ts)");
625
+ process.exit(1);
626
+ }
627
+ if (!fs3.existsSync(schemaDir)) {
628
+ fs3.mkdirSync(schemaDir, { recursive: true });
629
+ console.log(`\u2713 Created ${outputDir}/`);
630
+ }
631
+ const authSchemaContent = generateSchemaFile();
632
+ const authSchemaPath = path2.join(schemaDir, "auth.ts");
633
+ fs3.writeFileSync(authSchemaPath, authSchemaContent);
634
+ console.log(`\u2713 Created ${outputDir}/auth.ts`);
635
+ const extensionsPath = path2.join(schemaDir, "auth-extensions.ts");
636
+ if (!fs3.existsSync(extensionsPath)) {
637
+ fs3.writeFileSync(extensionsPath, generateExtensionsFile());
638
+ console.log(`\u2713 Created ${outputDir}/auth-extensions.ts`);
639
+ } else {
640
+ console.log(`\u2713 Preserved existing ${outputDir}/auth-extensions.ts`);
641
+ }
642
+ const indexPath = path2.join(schemaDir, "index.ts");
643
+ if (!fs3.existsSync(indexPath)) {
644
+ fs3.writeFileSync(indexPath, generateIndexFile());
645
+ console.log(`\u2713 Created ${outputDir}/index.ts`);
646
+ } else {
647
+ console.log(`\u2713 Preserved existing ${outputDir}/index.ts`);
648
+ }
649
+ const schemaHash = hashContent(authSchemaContent);
650
+ const config = createInitialConfig(getLibraryVersion(), schemaHash);
651
+ config.extensions.file = `${outputDir}/auth-extensions.ts`;
652
+ saveConfig(config, cwd);
653
+ console.log(`\u2713 Created syncello-auth.json`);
654
+ console.log("");
655
+ console.log("\u2705 @syncello/auth initialized successfully!");
656
+ console.log("");
657
+ console.log("Next steps:");
658
+ console.log(" 1. Run: pnpm drizzle-kit generate");
659
+ console.log(" 2. Run: pnpm drizzle-kit migrate");
660
+ console.log(" 3. Import schema in your app:");
661
+ console.log(` import * as schema from './${outputDir}';`);
662
+ }
663
+
664
+ // src/cli/commands/sync.ts
665
+ import * as fs4 from "fs";
666
+ import * as path3 from "path";
667
+
668
+ // src/cli/utils/diff.ts
669
+ import * as Diff from "diff";
670
+ import pc from "picocolors";
671
+ function generateDiff(oldContent, newContent, options = {}) {
672
+ const { oldLabel = "current", newLabel = "updated", context = 3 } = options;
673
+ if (!oldContent) {
674
+ const lines2 = newContent.split("\n");
675
+ const output = [
676
+ pc.bold(pc.cyan(`--- /dev/null`)),
677
+ pc.bold(pc.cyan(`+++ ${newLabel}`)),
678
+ pc.cyan(`@@ -0,0 +1,${lines2.length} @@`),
679
+ ...lines2.map((line) => pc.green(`+ ${line}`))
680
+ ].join("\n");
681
+ return {
682
+ output,
683
+ stats: { additions: lines2.length, deletions: 0 },
684
+ hasChanges: true
685
+ };
686
+ }
687
+ const patch = Diff.structuredPatch(oldLabel, newLabel, oldContent, newContent, void 0, void 0, { context });
688
+ if (patch.hunks.length === 0) {
689
+ return {
690
+ output: "",
691
+ stats: { additions: 0, deletions: 0 },
692
+ hasChanges: false
693
+ };
694
+ }
695
+ const lines = [];
696
+ let additions = 0;
697
+ let deletions = 0;
698
+ lines.push(pc.bold(`--- ${oldLabel}`));
699
+ lines.push(pc.bold(`+++ ${newLabel}`));
700
+ for (const hunk of patch.hunks) {
701
+ const hunkHeader = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
702
+ lines.push(pc.cyan(hunkHeader));
703
+ for (const line of hunk.lines) {
704
+ const prefix = line[0];
705
+ const content = line.slice(1);
706
+ switch (prefix) {
707
+ case "+":
708
+ lines.push(pc.green(`+ ${content}`));
709
+ additions++;
710
+ break;
711
+ case "-":
712
+ lines.push(pc.red(`- ${content}`));
713
+ deletions++;
714
+ break;
715
+ case " ":
716
+ lines.push(pc.dim(` ${content}`));
717
+ break;
718
+ case "\\":
719
+ lines.push(pc.dim(`\\ ${content}`));
720
+ break;
721
+ }
722
+ }
723
+ }
724
+ return {
725
+ output: lines.join("\n"),
726
+ stats: { additions, deletions },
727
+ hasChanges: true
728
+ };
729
+ }
730
+ function formatStats(stats) {
731
+ const parts = [];
732
+ if (stats.additions > 0) {
733
+ parts.push(pc.green(`+${stats.additions}`));
734
+ }
735
+ if (stats.deletions > 0) {
736
+ parts.push(pc.red(`-${stats.deletions}`));
737
+ }
738
+ if (parts.length === 0) {
739
+ return pc.dim("No changes");
740
+ }
741
+ return parts.join(" ");
742
+ }
743
+
744
+ // src/cli/utils/breaking-changes.ts
745
+ import pc2 from "picocolors";
746
+ function parseGeneratedSchema(content) {
747
+ const result = { tables: {}, enums: {} };
748
+ const enumRegex = /export const \w+ = pgEnum\('(\w+)',\s*\[([^\]]+)\]\)/g;
749
+ let enumMatch;
750
+ while ((enumMatch = enumRegex.exec(content)) !== null) {
751
+ const enumName = enumMatch[1];
752
+ const valuesStr = enumMatch[2];
753
+ const values = valuesStr.match(/'([^']+)'/g)?.map((v) => v.replace(/'/g, "")) || [];
754
+ result.enums[enumName] = { values };
755
+ }
756
+ const tableRegex = /export const (\w+) = pgTable\(\s*'(\w+)',\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gs;
757
+ let tableMatch;
758
+ while ((tableMatch = tableRegex.exec(content)) !== null) {
759
+ const tableName = tableMatch[2];
760
+ const columnsBlock = tableMatch[3];
761
+ result.tables[tableName] = { columns: parseColumns(columnsBlock) };
762
+ }
763
+ return result;
764
+ }
765
+ function parseColumns(block) {
766
+ const columns = {};
767
+ const lines = block.split("\n");
768
+ for (const line of lines) {
769
+ const colMatch = line.match(/(\w+):\s*(uuid|varchar|text|boolean|integer|bigint|timestamp|json|serial)\(/);
770
+ if (colMatch) {
771
+ const colName = colMatch[1];
772
+ const colType = colMatch[2];
773
+ const col = { type: colType };
774
+ const lengthMatch = line.match(/length:\s*(\d+)/);
775
+ if (lengthMatch) {
776
+ col.length = parseInt(lengthMatch[1], 10);
777
+ }
778
+ if (line.includes(".notNull()")) {
779
+ col.notNull = true;
780
+ }
781
+ if (line.includes(".unique()")) {
782
+ col.unique = true;
783
+ }
784
+ columns[colName] = col;
785
+ }
786
+ }
787
+ return columns;
788
+ }
789
+ function detectBreakingChanges(oldSchemaContent) {
790
+ const changes = [];
791
+ const oldSchema = parseGeneratedSchema(oldSchemaContent);
792
+ for (const tableName of Object.keys(oldSchema.tables)) {
793
+ const newTableKey = Object.entries(tables).find(
794
+ ([, def]) => def === tables[tableName]
795
+ )?.[0];
796
+ const tableExists = Object.values(tables).some(
797
+ (t) => t === tables[tableName]
798
+ );
799
+ const stillExists = tableName in getTableNameMapping();
800
+ if (!stillExists) {
801
+ changes.push({
802
+ type: "error",
803
+ category: "table",
804
+ message: `Table '${tableName}' has been removed`,
805
+ table: tableName
806
+ });
807
+ }
808
+ }
809
+ for (const [tableName, oldTable] of Object.entries(oldSchema.tables)) {
810
+ const newTable = getNewTableByName(tableName);
811
+ if (!newTable) continue;
812
+ for (const [colName, oldCol] of Object.entries(oldTable.columns)) {
813
+ const newCol = newTable.columns[colName];
814
+ if (!newCol) {
815
+ changes.push({
816
+ type: "error",
817
+ category: "column",
818
+ message: `Column '${colName}' removed from table '${tableName}'`,
819
+ table: tableName,
820
+ column: colName
821
+ });
822
+ continue;
823
+ }
824
+ if (normalizeType(oldCol.type) !== normalizeType(newCol.type)) {
825
+ changes.push({
826
+ type: "error",
827
+ category: "column",
828
+ message: `Column '${tableName}.${colName}' type changed: ${oldCol.type} \u2192 ${newCol.type}`,
829
+ table: tableName,
830
+ column: colName
831
+ });
832
+ }
833
+ if (oldCol.length && newCol.length && newCol.length < oldCol.length) {
834
+ changes.push({
835
+ type: "error",
836
+ category: "column",
837
+ message: `Column '${tableName}.${colName}' length reduced: ${oldCol.length} \u2192 ${newCol.length}`,
838
+ table: tableName,
839
+ column: colName
840
+ });
841
+ }
842
+ if (!oldCol.notNull && newCol.notNull && newCol.default === void 0) {
843
+ changes.push({
844
+ type: "error",
845
+ category: "constraint",
846
+ message: `Column '${tableName}.${colName}' made NOT NULL without default value`,
847
+ table: tableName,
848
+ column: colName
849
+ });
850
+ }
851
+ if (oldCol.unique && !newCol.unique) {
852
+ changes.push({
853
+ type: "warning",
854
+ category: "constraint",
855
+ message: `Column '${tableName}.${colName}' unique constraint removed`,
856
+ table: tableName,
857
+ column: colName
858
+ });
859
+ }
860
+ }
861
+ }
862
+ for (const [enumName, oldEnum] of Object.entries(oldSchema.enums)) {
863
+ const newEnum = Object.values(enums).find((e) => e.name === enumName);
864
+ if (!newEnum) {
865
+ changes.push({
866
+ type: "error",
867
+ category: "enum",
868
+ message: `Enum '${enumName}' has been removed`
869
+ });
870
+ continue;
871
+ }
872
+ for (const oldValue of oldEnum.values) {
873
+ if (!newEnum.values.includes(oldValue)) {
874
+ changes.push({
875
+ type: "error",
876
+ category: "enum",
877
+ message: `Value '${oldValue}' removed from enum '${enumName}'`
878
+ });
879
+ }
880
+ }
881
+ }
882
+ return {
883
+ changes,
884
+ hasErrors: changes.some((c) => c.type === "error"),
885
+ hasWarnings: changes.some((c) => c.type === "warning")
886
+ };
887
+ }
888
+ function getTableNameMapping() {
889
+ return {
890
+ users: "users",
891
+ sessions: "sessions",
892
+ user_2fa_methods: "user2faMethods",
893
+ user_backup_codes: "userBackupCodes",
894
+ user_trusted_devices: "userTrustedDevices",
895
+ email_verification_tokens: "emailVerificationTokens",
896
+ password_reset_tokens: "passwordResetTokens",
897
+ email_change_tokens: "emailChangeTokens",
898
+ email_events: "emailEvents",
899
+ failed_login_attempts: "failedLoginAttempts",
900
+ security_audit_log: "securityAuditLog",
901
+ oauth_accounts: "oauthAccounts"
902
+ };
903
+ }
904
+ function getNewTableByName(dbTableName) {
905
+ const mapping = getTableNameMapping();
906
+ const jsName = mapping[dbTableName];
907
+ if (!jsName) return void 0;
908
+ return tables[jsName];
909
+ }
910
+ function normalizeType(type) {
911
+ if (type.includes("_enum") || type === "two_factor_method" || type === "device_type") {
912
+ return "enum";
913
+ }
914
+ return type;
915
+ }
916
+ function formatBreakingChanges(result) {
917
+ if (result.changes.length === 0) {
918
+ return pc2.green("\u2713 No breaking changes detected");
919
+ }
920
+ const lines = [];
921
+ const errors = result.changes.filter((c) => c.type === "error");
922
+ const warnings = result.changes.filter((c) => c.type === "warning");
923
+ if (errors.length > 0) {
924
+ lines.push(pc2.red(pc2.bold("Breaking Changes:")));
925
+ for (const error of errors) {
926
+ lines.push(pc2.red(` \u2717 ${error.message}`));
927
+ }
928
+ }
929
+ if (warnings.length > 0) {
930
+ if (errors.length > 0) lines.push("");
931
+ lines.push(pc2.yellow(pc2.bold("Warnings:")));
932
+ for (const warning of warnings) {
933
+ lines.push(pc2.yellow(` \u26A0 ${warning.message}`));
934
+ }
935
+ }
936
+ return lines.join("\n");
937
+ }
938
+
939
+ // src/cli/commands/sync.ts
940
+ function getLibraryVersion2() {
941
+ try {
942
+ const pkgPath = path3.resolve(__dirname, "../../../package.json");
943
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
944
+ return pkg.version;
945
+ } catch {
946
+ return "0.0.0";
947
+ }
948
+ }
949
+ async function sync(options) {
950
+ const cwd = process.cwd();
951
+ if (!configExists(cwd)) {
952
+ console.log("\u274C @syncello/auth is not initialized in this project.");
953
+ console.log(" Run: npx @syncello/auth init");
954
+ process.exit(1);
955
+ }
956
+ const config = loadConfig(cwd);
957
+ const schemaDir = path3.dirname(path3.join(cwd, config.extensions.file));
958
+ const authSchemaPath = path3.join(schemaDir, "auth.ts");
959
+ const newSchemaContent = generateSchemaFile();
960
+ const newSchemaHash = hashContent(newSchemaContent);
961
+ const currentHash = hashFile(authSchemaPath);
962
+ const libraryVersion = getLibraryVersion2();
963
+ console.log(`Current version: ${config.libraryVersion} (schema ${config.schemaVersion})`);
964
+ console.log(`Available version: ${libraryVersion} (schema ${SCHEMA_VERSION})`);
965
+ console.log("");
966
+ if (currentHash && currentHash !== config.schemaHash && !options.force) {
967
+ console.log("\u26A0\uFE0F Warning: auth.ts has been modified locally.");
968
+ console.log(" Use --force to overwrite local changes.");
969
+ if (!options.dryRun) {
970
+ process.exit(1);
971
+ }
972
+ }
973
+ const currentContent = fs4.existsSync(authSchemaPath) ? fs4.readFileSync(authSchemaPath, "utf-8") : "";
974
+ if (newSchemaHash === config.schemaHash && config.schemaVersion === SCHEMA_VERSION) {
975
+ console.log("\u2705 Schema is already up to date.");
976
+ return;
977
+ }
978
+ if (options.diff || options.dryRun) {
979
+ console.log("Schema changes:");
980
+ if (config.schemaVersion !== SCHEMA_VERSION) {
981
+ console.log(` Schema version: ${config.schemaVersion} \u2192 ${SCHEMA_VERSION}`);
982
+ }
983
+ console.log("");
984
+ const diff = generateDiff(currentContent, newSchemaContent, {
985
+ oldLabel: "auth.ts (current)",
986
+ newLabel: "auth.ts (updated)",
987
+ context: 3
988
+ });
989
+ if (diff.hasChanges) {
990
+ console.log(diff.output);
991
+ console.log("");
992
+ console.log(`Summary: ${formatStats(diff.stats)}`);
993
+ } else {
994
+ console.log(" No content changes detected.");
995
+ }
996
+ console.log("");
997
+ }
998
+ if (currentContent) {
999
+ const breakingChanges = detectBreakingChanges(currentContent);
1000
+ if (breakingChanges.hasErrors || breakingChanges.hasWarnings) {
1001
+ console.log(formatBreakingChanges(breakingChanges));
1002
+ console.log("");
1003
+ if (breakingChanges.hasErrors && !options.force) {
1004
+ console.log("\u274C Breaking changes detected. Migration required.");
1005
+ console.log(" Use --force to sync anyway (not recommended).");
1006
+ console.log("");
1007
+ console.log("Recommended steps:");
1008
+ console.log(" 1. Review the breaking changes above");
1009
+ console.log(" 2. Create a migration plan for affected data");
1010
+ console.log(" 3. Run with --force after preparing migration");
1011
+ process.exit(1);
1012
+ }
1013
+ } else {
1014
+ console.log("\u2713 No breaking changes detected");
1015
+ console.log("");
1016
+ }
1017
+ }
1018
+ if (options.dryRun) {
1019
+ console.log("Dry run complete. No files modified.");
1020
+ return;
1021
+ }
1022
+ if (fs4.existsSync(authSchemaPath)) {
1023
+ const backupPath = authSchemaPath + ".backup";
1024
+ fs4.copyFileSync(authSchemaPath, backupPath);
1025
+ console.log(`\u2713 Backed up auth.ts \u2192 auth.ts.backup`);
1026
+ }
1027
+ fs4.writeFileSync(authSchemaPath, newSchemaContent);
1028
+ console.log(`\u2713 Updated auth.ts`);
1029
+ config.libraryVersion = libraryVersion;
1030
+ config.schemaVersion = SCHEMA_VERSION;
1031
+ config.schemaHash = newSchemaHash;
1032
+ config.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
1033
+ saveConfig(config, cwd);
1034
+ console.log(`\u2713 Updated syncello-auth.json`);
1035
+ console.log("");
1036
+ console.log("\u2705 Sync complete!");
1037
+ console.log("");
1038
+ console.log("Next steps:");
1039
+ console.log(" 1. Run: pnpm drizzle-kit generate");
1040
+ console.log(" 2. Review the migration");
1041
+ console.log(" 3. Run: pnpm drizzle-kit migrate");
1042
+ }
1043
+
1044
+ // src/cli/commands/status.ts
1045
+ import * as fs5 from "fs";
1046
+ import * as path4 from "path";
1047
+ function getLibraryVersion3() {
1048
+ try {
1049
+ const pkgPath = path4.resolve(__dirname, "../../../package.json");
1050
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1051
+ return pkg.version;
1052
+ } catch {
1053
+ return "0.0.0";
1054
+ }
1055
+ }
1056
+ async function status() {
1057
+ const cwd = process.cwd();
1058
+ if (!configExists(cwd)) {
1059
+ console.log("\u274C @syncello/auth is not initialized in this project.");
1060
+ console.log(" Run: npx @syncello/auth init");
1061
+ process.exit(1);
1062
+ }
1063
+ const config = loadConfig(cwd);
1064
+ const libraryVersion = getLibraryVersion3();
1065
+ const schemaDir = path4.dirname(path4.join(cwd, config.extensions.file));
1066
+ const authSchemaPath = path4.join(schemaDir, "auth.ts");
1067
+ console.log("@syncello/auth Status");
1068
+ console.log("\u2500".repeat(40));
1069
+ console.log("");
1070
+ console.log("Installed:");
1071
+ console.log(` Library version: ${config.libraryVersion}`);
1072
+ console.log(` Schema version: ${config.schemaVersion}`);
1073
+ console.log(` Last synced: ${config.lastSyncedAt}`);
1074
+ console.log("");
1075
+ console.log("Available:");
1076
+ console.log(` Library version: ${libraryVersion}`);
1077
+ console.log(` Schema version: ${SCHEMA_VERSION}`);
1078
+ console.log("");
1079
+ const currentHash = hashFile(authSchemaPath);
1080
+ if (currentHash && currentHash !== config.schemaHash) {
1081
+ console.log("\u26A0\uFE0F auth.ts has local modifications");
1082
+ }
1083
+ if (config.schemaVersion !== SCHEMA_VERSION) {
1084
+ console.log("\u{1F4E6} Schema update available!");
1085
+ console.log(" Run: npx @syncello/auth sync");
1086
+ } else if (config.libraryVersion !== libraryVersion) {
1087
+ console.log("\u{1F4E6} Library update available");
1088
+ console.log(" Run: pnpm update @syncello/auth");
1089
+ } else {
1090
+ console.log("\u2705 Up to date");
1091
+ }
1092
+ }
1093
+
1094
+ // src/cli/index.ts
1095
+ var program = new Command();
1096
+ program.name("syncello-auth").description("CLI for @syncello/auth schema management").version("0.0.0");
1097
+ program.command("init").description("Initialize @syncello/auth in your project").option("-o, --output <dir>", "Output directory for schema files", "db/schema").option("-f, --force", "Force reinitialize (overwrites auth.ts)").action(init);
1098
+ program.command("sync").description("Sync schema to latest version").option("-d, --dry-run", "Preview changes without modifying files").option("-f, --force", "Force sync even if local modifications exist").option("--diff", "Show detailed diff of changes").action(sync);
1099
+ program.command("status").description("Show current schema status").action(status);
1100
+ program.parse();