@pikku/cli 0.12.35 → 0.12.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.schema.json +1 -1
- package/console-app/assets/{index-BOM3RFeu.js → index-Dxl3JsMK.js} +73 -73
- package/console-app/index.html +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +6 -0
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +2 -2
- package/dist/.pikku/function/pikku-function-types.gen.js +17 -3
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +140 -123
- package/dist/.pikku/function/pikku-functions.gen.js +3 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +3 -2
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +11 -7
- package/dist/.pikku/schemas/schemas/DbGenerateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuFunctionTypesInput.schema.json +1 -0
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +22 -4
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +5 -0
- package/dist/src/fabric/functions/login.function.d.ts +1 -1
- package/dist/src/fabric/functions/login.function.js +1 -1
- package/dist/src/functions/commands/bootstrap.js +1 -1
- package/dist/src/functions/commands/db-generate.d.ts +1 -0
- package/dist/src/functions/commands/db-generate.js +45 -0
- package/dist/src/functions/commands/db-migrate.js +13 -1
- package/dist/src/functions/db/better-auth-schema.d.ts +23 -0
- package/dist/src/functions/db/better-auth-schema.js +122 -0
- package/dist/src/functions/db/local-db.d.ts +33 -0
- package/dist/src/functions/db/local-db.js +125 -1
- package/dist/src/functions/db/zod-codegen.js +9 -6
- package/dist/src/functions/validate/workspace-validate.js +1 -1
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +30 -4
- package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +33 -1
- package/dist/src/functions/wirings/auth/serialize-auth-gen.js +122 -88
- package/dist/src/functions/wirings/auth/serialize-auth-meta.d.ts +32 -0
- package/dist/src/functions/wirings/auth/serialize-auth-meta.js +23 -0
- package/dist/src/functions/wirings/auth/serialize-auth-types.d.ts +27 -0
- package/dist/src/functions/wirings/auth/serialize-auth-types.js +58 -0
- package/dist/src/functions/wirings/functions/pikku-command-function-types.d.ts +7 -1
- package/dist/src/functions/wirings/functions/pikku-command-function-types.js +16 -3
- package/dist/src/functions/wirings/functions/pikku-command-services.d.ts +1 -1
- package/dist/src/functions/wirings/functions/pikku-command-services.js +9 -2
- package/dist/src/functions/wirings/functions/serialize-function-types.js +17 -3
- package/dist/src/functions/wirings/functions/serialize-pikku-types-hub.d.ts +1 -1
- package/dist/src/functions/wirings/functions/serialize-pikku-types-hub.js +2 -1
- package/dist/src/functions/workflows/all.workflow.js +16 -2
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/services.js +8 -0
- package/dist/src/utils/pikku-cli-config.js +12 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/skills/pikku-better-auth/SKILL.md +211 -0
- package/skills/pikku-auth-js/SKILL.md +0 -339
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
+
import { resolveDb, generateAuthMigration } from '../db/local-db.js';
|
|
3
|
+
import { loadUserConfigForDb } from './db-shared.js';
|
|
4
|
+
export const dbGenerate = pikkuSessionlessFunc({
|
|
5
|
+
remote: true,
|
|
6
|
+
func: async ({ logger, config }) => {
|
|
7
|
+
const userConfig = await loadUserConfigForDb({ config, logger });
|
|
8
|
+
if (!userConfig)
|
|
9
|
+
return;
|
|
10
|
+
const resolved = resolveDb(userConfig, config.rootDir, config.outDir, config.runtimeDir);
|
|
11
|
+
if (!resolved) {
|
|
12
|
+
logger.error('pikku db generate: no database configured — set sqliteDb or postgresUrl in your createConfig.');
|
|
13
|
+
throw new Error('no database configured');
|
|
14
|
+
}
|
|
15
|
+
const result = await generateAuthMigration(resolved, config.rootDir, config.srcDirectories, logger);
|
|
16
|
+
switch (result.status) {
|
|
17
|
+
case 'no-auth':
|
|
18
|
+
logger.info('db generate: no pikkuBetterAuth found — nothing to generate');
|
|
19
|
+
return;
|
|
20
|
+
case 'up-to-date':
|
|
21
|
+
logger.info('db generate: Better Auth schema already covered by existing migrations — nothing to generate');
|
|
22
|
+
return;
|
|
23
|
+
case 'unsupported-dialect':
|
|
24
|
+
logger.warn('db generate: automatic Better Auth migration generation is currently SQLite-only. Run Better Auth schema generation manually for postgres.');
|
|
25
|
+
return;
|
|
26
|
+
case 'incremental-unsupported': {
|
|
27
|
+
const cols = (result.missingColumns ?? [])
|
|
28
|
+
.map((m) => `${m.table}(${m.columns.join(', ')})`)
|
|
29
|
+
.join('; ');
|
|
30
|
+
logger.error('db generate: the Better Auth config requires schema changes on top of an existing auth schema:');
|
|
31
|
+
if (result.missingTables?.length) {
|
|
32
|
+
logger.error(` missing tables: ${result.missingTables.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
if (cols)
|
|
35
|
+
logger.error(` missing columns: ${cols}`);
|
|
36
|
+
logger.error(' Write a forward migration adding these by hand (incremental auto-generation is not yet supported).');
|
|
37
|
+
throw new Error('incremental auth schema change requires a manual migration');
|
|
38
|
+
}
|
|
39
|
+
case 'written':
|
|
40
|
+
logger.info(`db generate: wrote ${result.file}`);
|
|
41
|
+
logger.info(' Review it, then run `pikku db migrate` to apply.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
|
-
import { resolveDb, migrateAndCodegen } from '../db/local-db.js';
|
|
2
|
+
import { resolveDb, migrateAndCodegen, computeAuthDrift, } from '../db/local-db.js';
|
|
3
3
|
import { loadUserConfigForDb } from './db-shared.js';
|
|
4
4
|
export const dbMigrate = pikkuSessionlessFunc({
|
|
5
5
|
remote: true,
|
|
@@ -27,5 +27,17 @@ export const dbMigrate = pikkuSessionlessFunc({
|
|
|
27
27
|
logger.info(zod.written
|
|
28
28
|
? `db migrate: regenerated ${zod.outFile} (${zod.tables.length} tables)`
|
|
29
29
|
: `db migrate: ${zod.outFile} unchanged`);
|
|
30
|
+
const drift = await computeAuthDrift(resolved, config.rootDir, config.srcDirectories, logger);
|
|
31
|
+
if (drift.hasAuth && !drift.inSync) {
|
|
32
|
+
logger.error('db migrate: the Better Auth schema is not satisfied by the applied migrations:');
|
|
33
|
+
if (drift.missingTables.length) {
|
|
34
|
+
logger.error(` missing tables: ${drift.missingTables.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
for (const { table, columns } of drift.missingColumns) {
|
|
37
|
+
logger.error(` ${table} missing columns: ${columns.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
logger.error(' Run `pikku db generate` to create the missing migration, then re-run.');
|
|
40
|
+
throw new Error('Better Auth schema drift — run `pikku db generate`');
|
|
41
|
+
}
|
|
30
42
|
},
|
|
31
43
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
export interface BetterAuthOptionsLike {
|
|
3
|
+
database?: {
|
|
4
|
+
db?: unknown;
|
|
5
|
+
type?: string;
|
|
6
|
+
};
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface GetMigrationsResult {
|
|
10
|
+
toBeCreated: unknown[];
|
|
11
|
+
toBeAdded: unknown[];
|
|
12
|
+
runMigrations: () => Promise<void>;
|
|
13
|
+
compileMigrations: () => Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadAuthOptions(opts: {
|
|
16
|
+
rootDir: string;
|
|
17
|
+
srcDirectories: string[];
|
|
18
|
+
kysely: Kysely<any>;
|
|
19
|
+
logger: {
|
|
20
|
+
error: (msg: string) => void;
|
|
21
|
+
};
|
|
22
|
+
}): Promise<BetterAuthOptionsLike | null>;
|
|
23
|
+
export declare function getAuthMigrations(authOptions: BetterAuthOptionsLike): Promise<GetMigrationsResult>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join, extname, dirname } from 'node:path';
|
|
5
|
+
import { PIKKU_BETTER_AUTH } from '@pikku/better-auth';
|
|
6
|
+
import { loadUserModule } from '../commands/load-user-project.js';
|
|
7
|
+
let cachedGetMigrations = null;
|
|
8
|
+
async function loadGetMigrations() {
|
|
9
|
+
if (cachedGetMigrations)
|
|
10
|
+
return cachedGetMigrations;
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const mainEntry = require.resolve('better-auth');
|
|
13
|
+
let root = dirname(mainEntry);
|
|
14
|
+
while (!existsSync(join(root, 'package.json'))) {
|
|
15
|
+
const parent = dirname(root);
|
|
16
|
+
if (parent === root) {
|
|
17
|
+
throw new Error('Could not locate the better-auth package root');
|
|
18
|
+
}
|
|
19
|
+
root = parent;
|
|
20
|
+
}
|
|
21
|
+
const modUrl = pathToFileURL(join(root, 'dist/db/get-migration.mjs')).href;
|
|
22
|
+
const mod = await import(modUrl);
|
|
23
|
+
cachedGetMigrations = mod.getMigrations;
|
|
24
|
+
return cachedGetMigrations;
|
|
25
|
+
}
|
|
26
|
+
const SKIP_DIRS = new Set([
|
|
27
|
+
'node_modules',
|
|
28
|
+
'.pikku',
|
|
29
|
+
'.git',
|
|
30
|
+
'dist',
|
|
31
|
+
'.pikku-runtime',
|
|
32
|
+
]);
|
|
33
|
+
function findAuthSourceFile(rootDir, srcDirectories) {
|
|
34
|
+
const walk = (dir) => {
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = readdirSync(dir);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (SKIP_DIRS.has(entry))
|
|
44
|
+
continue;
|
|
45
|
+
const full = join(dir, entry);
|
|
46
|
+
let st;
|
|
47
|
+
try {
|
|
48
|
+
st = statSync(full);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (st.isDirectory()) {
|
|
54
|
+
const found = walk(full);
|
|
55
|
+
if (found)
|
|
56
|
+
return found;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (extname(full) !== '.ts')
|
|
60
|
+
continue;
|
|
61
|
+
let src;
|
|
62
|
+
try {
|
|
63
|
+
src = readFileSync(full, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (/\bpikkuBetterAuth\s*\(/.test(src))
|
|
69
|
+
return full;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
for (const srcDir of srcDirectories) {
|
|
74
|
+
const found = walk(join(rootDir, srcDir));
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
return walk(rootDir);
|
|
79
|
+
}
|
|
80
|
+
async function loadAuthFactory(sourceFile) {
|
|
81
|
+
const mod = await loadUserModule(sourceFile);
|
|
82
|
+
for (const value of Object.values(mod)) {
|
|
83
|
+
if (typeof value === 'function' && value[PIKKU_BETTER_AUTH]) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function schemaServicesStub(kysely, logger) {
|
|
90
|
+
const dummy = 'x'.repeat(32);
|
|
91
|
+
const fromKeys = (keys) => Object.fromEntries(keys.map((k) => [k, dummy]));
|
|
92
|
+
const base = {
|
|
93
|
+
kysely,
|
|
94
|
+
logger,
|
|
95
|
+
secrets: {
|
|
96
|
+
getSecret: async () => dummy,
|
|
97
|
+
getSecrets: async (keys) => fromKeys(keys),
|
|
98
|
+
},
|
|
99
|
+
variables: {
|
|
100
|
+
getVariable: async () => dummy,
|
|
101
|
+
getVariables: async (keys) => fromKeys(keys),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
return new Proxy(base, {
|
|
105
|
+
get: (target, prop) => typeof prop === 'string' && prop in target ? target[prop] : undefined,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export async function loadAuthOptions(opts) {
|
|
109
|
+
const sourceFile = findAuthSourceFile(opts.rootDir, opts.srcDirectories);
|
|
110
|
+
if (!sourceFile)
|
|
111
|
+
return null;
|
|
112
|
+
const factory = await loadAuthFactory(sourceFile);
|
|
113
|
+
if (!factory)
|
|
114
|
+
return null;
|
|
115
|
+
const instance = await factory(schemaServicesStub(opts.kysely, opts.logger));
|
|
116
|
+
const options = instance.options;
|
|
117
|
+
return options ?? null;
|
|
118
|
+
}
|
|
119
|
+
export async function getAuthMigrations(authOptions) {
|
|
120
|
+
const getMigrations = await loadGetMigrations();
|
|
121
|
+
return getMigrations(authOptions);
|
|
122
|
+
}
|
|
@@ -53,4 +53,37 @@ export declare function migrateAndCodegen(resolved: ResolvedDb): Promise<Migrate
|
|
|
53
53
|
export declare function seed(resolved: ResolvedSqliteDb): Promise<SeedResult>;
|
|
54
54
|
export declare function reset(resolved: ResolvedSqliteDb, rootDir: string): void;
|
|
55
55
|
export declare function createKysely<DB>(resolved: ResolvedSqliteDb): Promise<Kysely<DB>>;
|
|
56
|
+
type SchemaMap = Map<string, Set<string>>;
|
|
57
|
+
export interface DesiredAuthSchema {
|
|
58
|
+
tables: SchemaMap;
|
|
59
|
+
sql: string;
|
|
60
|
+
}
|
|
61
|
+
export declare function desiredAuthSchema(rootDir: string, srcDirectories: string[], logger: {
|
|
62
|
+
error: (msg: string) => void;
|
|
63
|
+
}): Promise<DesiredAuthSchema | null>;
|
|
64
|
+
export declare function introspectSchema(resolved: ResolvedDb): Promise<SchemaMap>;
|
|
65
|
+
export interface AuthDriftResult {
|
|
66
|
+
hasAuth: boolean;
|
|
67
|
+
inSync: boolean;
|
|
68
|
+
missingTables: string[];
|
|
69
|
+
missingColumns: {
|
|
70
|
+
table: string;
|
|
71
|
+
columns: string[];
|
|
72
|
+
}[];
|
|
73
|
+
}
|
|
74
|
+
export declare function computeAuthDrift(resolved: ResolvedDb, rootDir: string, srcDirectories: string[], logger: {
|
|
75
|
+
error: (msg: string) => void;
|
|
76
|
+
}): Promise<AuthDriftResult>;
|
|
77
|
+
export interface GenerateAuthResult {
|
|
78
|
+
status: 'no-auth' | 'up-to-date' | 'written' | 'incremental-unsupported' | 'unsupported-dialect';
|
|
79
|
+
file?: string;
|
|
80
|
+
missingTables?: string[];
|
|
81
|
+
missingColumns?: {
|
|
82
|
+
table: string;
|
|
83
|
+
columns: string[];
|
|
84
|
+
}[];
|
|
85
|
+
}
|
|
86
|
+
export declare function generateAuthMigration(resolved: ResolvedDb, rootDir: string, srcDirectories: string[], logger: {
|
|
87
|
+
error: (msg: string) => void;
|
|
88
|
+
}): Promise<GenerateAuthResult>;
|
|
56
89
|
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, readdirSync, } from 'node:fs';
|
|
2
2
|
import { resolve, isAbsolute, relative, dirname, join } from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { runInNewContext } from 'node:vm';
|
|
5
5
|
import { transformSync } from 'esbuild';
|
|
6
6
|
import { migrate } from './db-migrator.js';
|
|
7
|
+
import { loadAuthOptions, getAuthMigrations } from './better-auth-schema.js';
|
|
7
8
|
import { generateSchemaTypes } from './db-codegen.js';
|
|
8
9
|
import { generateZodTypes } from './zod-codegen.js';
|
|
9
10
|
import { createCoercionPlugin } from './coercion-plugin.js';
|
|
@@ -295,3 +296,126 @@ export async function createKysely(resolved) {
|
|
|
295
296
|
plugins: coercionMap ? [createCoercionPlugin({ map: coercionMap })] : [],
|
|
296
297
|
});
|
|
297
298
|
}
|
|
299
|
+
async function introspectorToMap(intro) {
|
|
300
|
+
const map = new Map();
|
|
301
|
+
for (const table of await intro.listTables()) {
|
|
302
|
+
const cols = await intro.getColumns(table);
|
|
303
|
+
map.set(table, new Set(cols.map((c) => c.name)));
|
|
304
|
+
}
|
|
305
|
+
return map;
|
|
306
|
+
}
|
|
307
|
+
function diffSchemas(desired, actual) {
|
|
308
|
+
const missingTables = [];
|
|
309
|
+
const missingColumns = [];
|
|
310
|
+
for (const [table, cols] of desired) {
|
|
311
|
+
const actualCols = actual.get(table);
|
|
312
|
+
if (!actualCols) {
|
|
313
|
+
missingTables.push(table);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const missing = [...cols].filter((c) => !actualCols.has(c));
|
|
317
|
+
if (missing.length)
|
|
318
|
+
missingColumns.push({ table, columns: missing });
|
|
319
|
+
}
|
|
320
|
+
return { missingTables, missingColumns };
|
|
321
|
+
}
|
|
322
|
+
export async function desiredAuthSchema(rootDir, srcDirectories, logger) {
|
|
323
|
+
const runtime = await loadSqliteRuntime();
|
|
324
|
+
const db = runtime.open(':memory:');
|
|
325
|
+
try {
|
|
326
|
+
const kysely = createSqliteKysely({ db, camelCase: true });
|
|
327
|
+
const options = await loadAuthOptions({ rootDir, srcDirectories, kysely, logger });
|
|
328
|
+
if (!options)
|
|
329
|
+
return null;
|
|
330
|
+
const { runMigrations, compileMigrations } = await getAuthMigrations(options);
|
|
331
|
+
await runMigrations();
|
|
332
|
+
const tables = await introspectorToMap(new SqliteIntrospector(db));
|
|
333
|
+
const sql = await compileMigrations();
|
|
334
|
+
return { tables, sql };
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
db.close();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
export async function introspectSchema(resolved) {
|
|
341
|
+
if (resolved.dialect === 'sqlite') {
|
|
342
|
+
const runtime = await loadSqliteRuntime();
|
|
343
|
+
const db = runtime.open(resolved.dbFile);
|
|
344
|
+
try {
|
|
345
|
+
return await introspectorToMap(new SqliteIntrospector(db));
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
db.close();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const intro = new PostgresIntrospector(resolved.connectionString);
|
|
352
|
+
await intro.connect();
|
|
353
|
+
try {
|
|
354
|
+
return await introspectorToMap(intro);
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
await intro.close();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function coveredSqliteSchema(migrationsDir) {
|
|
361
|
+
const runtime = await loadSqliteRuntime();
|
|
362
|
+
const db = runtime.open(':memory:');
|
|
363
|
+
try {
|
|
364
|
+
await migrate(new SqliteMigrationExecutor(db), migrationsDir);
|
|
365
|
+
return await introspectorToMap(new SqliteIntrospector(db));
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
db.close();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
export async function computeAuthDrift(resolved, rootDir, srcDirectories, logger) {
|
|
372
|
+
const desired = await desiredAuthSchema(rootDir, srcDirectories, logger);
|
|
373
|
+
if (!desired) {
|
|
374
|
+
return { hasAuth: false, inSync: true, missingTables: [], missingColumns: [] };
|
|
375
|
+
}
|
|
376
|
+
const actual = await introspectSchema(resolved);
|
|
377
|
+
const { missingTables, missingColumns } = diffSchemas(desired.tables, actual);
|
|
378
|
+
return {
|
|
379
|
+
hasAuth: true,
|
|
380
|
+
inSync: missingTables.length === 0 && missingColumns.length === 0,
|
|
381
|
+
missingTables,
|
|
382
|
+
missingColumns,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function nextMigrationFile(migrationsDir, label) {
|
|
386
|
+
mkdirSync(migrationsDir, { recursive: true });
|
|
387
|
+
let max = 0;
|
|
388
|
+
try {
|
|
389
|
+
for (const file of readdirSync(migrationsDir)) {
|
|
390
|
+
const m = /^(\d+)/.exec(file);
|
|
391
|
+
if (m)
|
|
392
|
+
max = Math.max(max, parseInt(m[1], 10));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
max = 0;
|
|
397
|
+
}
|
|
398
|
+
const num = String(max + 1).padStart(4, '0');
|
|
399
|
+
return join(migrationsDir, `${num}-${label}.sql`);
|
|
400
|
+
}
|
|
401
|
+
export async function generateAuthMigration(resolved, rootDir, srcDirectories, logger) {
|
|
402
|
+
if (resolved.dialect !== 'sqlite')
|
|
403
|
+
return { status: 'unsupported-dialect' };
|
|
404
|
+
const desired = await desiredAuthSchema(rootDir, srcDirectories, logger);
|
|
405
|
+
if (!desired)
|
|
406
|
+
return { status: 'no-auth' };
|
|
407
|
+
const covered = await coveredSqliteSchema(resolved.migrationsDir);
|
|
408
|
+
const { missingTables, missingColumns } = diffSchemas(desired.tables, covered);
|
|
409
|
+
if (missingTables.length === 0 && missingColumns.length === 0) {
|
|
410
|
+
return { status: 'up-to-date' };
|
|
411
|
+
}
|
|
412
|
+
const coveredHasAnyAuthTable = [...desired.tables.keys()].some((t) => covered.has(t));
|
|
413
|
+
if (coveredHasAnyAuthTable) {
|
|
414
|
+
return { status: 'incremental-unsupported', missingTables, missingColumns };
|
|
415
|
+
}
|
|
416
|
+
const file = nextMigrationFile(resolved.migrationsDir, 'better-auth');
|
|
417
|
+
const header = '-- Generated by `pikku db generate` from pikkuBetterAuth (Better Auth).\n' +
|
|
418
|
+
'-- Re-run the command after changing the auth config.\n\n';
|
|
419
|
+
writeFileSync(file, header + desired.sql + '\n', 'utf8');
|
|
420
|
+
return { status: 'written', file, missingTables };
|
|
421
|
+
}
|
|
@@ -57,12 +57,15 @@ function parseTables(src) {
|
|
|
57
57
|
const tables = [];
|
|
58
58
|
for (const match of src.matchAll(INTERFACE_RE)) {
|
|
59
59
|
const name = match[1];
|
|
60
|
-
if (name === 'DB')
|
|
60
|
+
if (!name || name === 'DB')
|
|
61
61
|
continue;
|
|
62
|
-
const body = match[2];
|
|
62
|
+
const body = match[2] ?? '';
|
|
63
63
|
const fields = [];
|
|
64
64
|
for (const field of body.matchAll(FIELD_RE)) {
|
|
65
|
-
|
|
65
|
+
const fieldName = field[1];
|
|
66
|
+
if (!fieldName)
|
|
67
|
+
continue;
|
|
68
|
+
fields.push({ name: fieldName, type: (field[2] ?? '').trim() });
|
|
66
69
|
}
|
|
67
70
|
tables.push({ name, fields });
|
|
68
71
|
}
|
|
@@ -119,7 +122,7 @@ function zodForType(tsType, format) {
|
|
|
119
122
|
// Peel a single `Generated<…>` wrapper. For public bool/date columns this
|
|
120
123
|
// wraps a `ColumnType<…>`, so the unwrapped inner is handled below.
|
|
121
124
|
const generatedMatch = inner.match(/^Generated<(.+)>$/);
|
|
122
|
-
if (generatedMatch) {
|
|
125
|
+
if (generatedMatch?.[1]) {
|
|
123
126
|
generated = true;
|
|
124
127
|
inner = generatedMatch[1].trim();
|
|
125
128
|
}
|
|
@@ -146,7 +149,7 @@ function scalarSchema(tsType, format) {
|
|
|
146
149
|
let inner = tsType.trim();
|
|
147
150
|
// Defensive: a Select arg may itself be `Generated<…>` in older schemas.
|
|
148
151
|
const generatedMatch = inner.match(/^Generated<(.+)>$/);
|
|
149
|
-
if (generatedMatch) {
|
|
152
|
+
if (generatedMatch?.[1]) {
|
|
150
153
|
inner = generatedMatch[1].trim();
|
|
151
154
|
}
|
|
152
155
|
const nullable = inner.endsWith(' | null');
|
|
@@ -155,7 +158,7 @@ function scalarSchema(tsType, format) {
|
|
|
155
158
|
}
|
|
156
159
|
// Unwrap a classification brand: `Private<T>` / `Pii<T>` / `Secret<T>` → T.
|
|
157
160
|
const brandMatch = inner.match(/^(?:Private|Pii|Secret)<(.+)>$/);
|
|
158
|
-
if (brandMatch) {
|
|
161
|
+
if (brandMatch?.[1]) {
|
|
159
162
|
inner = brandMatch[1].trim();
|
|
160
163
|
}
|
|
161
164
|
let schema;
|
|
@@ -61,7 +61,7 @@ async function hasAuthSessionMiddleware(fnDir) {
|
|
|
61
61
|
const meta = await readJsonSafe(metaPath);
|
|
62
62
|
if (!meta?.instances)
|
|
63
63
|
return false;
|
|
64
|
-
return Object.values(meta.instances).some((instance) => instance.definitionId === '
|
|
64
|
+
return Object.values(meta.instances).some((instance) => instance.definitionId === 'betterAuthSession');
|
|
65
65
|
}
|
|
66
66
|
function migrationCreatesTable(sql, tableName) {
|
|
67
67
|
const escapedTable = tableName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
@@ -1,17 +1,43 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
1
2
|
import { pikkuSessionlessFunc } from '#pikku';
|
|
2
3
|
import { writeFileInDir } from '../../../utils/file-writer.js';
|
|
3
4
|
import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
|
|
4
5
|
import { serializeAuthGen } from './serialize-auth-gen.js';
|
|
6
|
+
import { serializeAuthTypes } from './serialize-auth-types.js';
|
|
7
|
+
import { serializeAuthMeta } from './serialize-auth-meta.js';
|
|
5
8
|
export const pikkuAuth = pikkuSessionlessFunc({
|
|
6
9
|
func: async ({ logger, config, getInspectorState }) => {
|
|
7
|
-
const { authFile } = config;
|
|
10
|
+
const { authFile, authTypesFile, authMetaJsonFile, functionTypesFile, typesDeclarationFile, secretsFile: secretsServiceFile, variablesFile, packageMappings, } = config;
|
|
8
11
|
if (!authFile)
|
|
9
12
|
return;
|
|
10
13
|
const state = await getInspectorState();
|
|
11
|
-
|
|
14
|
+
// Only generate when the project declares auth via `pikkuBetterAuth`. Gating on
|
|
15
|
+
// the definition (not provider count) means credentials-only auth — which
|
|
16
|
+
// has no OAuth providers — still generates its /auth/* wiring.
|
|
17
|
+
if (!state.auth.definition)
|
|
12
18
|
return;
|
|
13
|
-
const
|
|
14
|
-
|
|
19
|
+
const { wiring, secrets } = serializeAuthGen(state.auth.definition, state.auth.providers, authFile, typesDeclarationFile, packageMappings ?? {});
|
|
20
|
+
// The secrets file sits alongside authFile so re-inspection rediscovers it.
|
|
21
|
+
// It is kept separate from the wiring file because the CLI forbids Zod
|
|
22
|
+
// schemas and HTTP wiring (wireHTTPRoutes) in the same file (PKU490).
|
|
23
|
+
const secretsFile = join(dirname(authFile), 'auth-secrets.gen.ts');
|
|
24
|
+
await writeFileInDir(logger, authFile, wiring);
|
|
25
|
+
await writeFileInDir(logger, secretsFile, secrets);
|
|
26
|
+
// Static metadata of the enabled providers/plugins for the console SSO page,
|
|
27
|
+
// following the `*-meta.gen.json` convention. Read at runtime by the console
|
|
28
|
+
// getAuthProviders function instead of a runtime registry.
|
|
29
|
+
if (authMetaJsonFile) {
|
|
30
|
+
const meta = serializeAuthMeta(state.auth.definition, state.auth.providers);
|
|
31
|
+
await writeFileInDir(logger, authMetaJsonFile, JSON.stringify(meta, null, 2));
|
|
32
|
+
}
|
|
33
|
+
// Generate the typed pikkuBetterAuth re-export consumed by `import { pikkuBetterAuth } from '#pikku'`.
|
|
34
|
+
if (authTypesFile &&
|
|
35
|
+
functionTypesFile &&
|
|
36
|
+
secretsServiceFile &&
|
|
37
|
+
variablesFile) {
|
|
38
|
+
const authTypes = serializeAuthTypes(authTypesFile, functionTypesFile, secretsServiceFile, variablesFile, packageMappings ?? {});
|
|
39
|
+
await writeFileInDir(logger, authTypesFile, authTypes);
|
|
40
|
+
}
|
|
15
41
|
},
|
|
16
42
|
middleware: [
|
|
17
43
|
logCommandInfoAndTime({
|
|
@@ -1 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
import type { AuthDefinition } from '@pikku/inspector';
|
|
2
|
+
/**
|
|
3
|
+
* The two files generated from a `pikkuBetterAuth` export.
|
|
4
|
+
*
|
|
5
|
+
* They are split because the CLI's schema/wiring-separation rule (PKU490)
|
|
6
|
+
* forbids a file from declaring Zod schemas AND `wireHTTPRoutes` together —
|
|
7
|
+
* schema files are imported at runtime, which would fire HTTP wiring
|
|
8
|
+
* side-effects without a server context. `wireSecret`/`wireVariable` are NOT
|
|
9
|
+
* HTTP wiring, so schemas may sit alongside them; only the route wiring must be
|
|
10
|
+
* separated out.
|
|
11
|
+
*/
|
|
12
|
+
export interface AuthGenOutput {
|
|
13
|
+
/** The HTTP wiring file (authFile): handler + catch-all routes + session middleware. */
|
|
14
|
+
wiring: string;
|
|
15
|
+
/** The secrets file: Zod schemas + wireSecret/wireVariable. */
|
|
16
|
+
secrets: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generates the `auth.gen.ts` (HTTP wiring) and `auth-secrets.gen.ts` (schemas +
|
|
20
|
+
* secret/variable wiring) files from a `pikkuBetterAuth((services) => betterAuth(...))`
|
|
21
|
+
* export.
|
|
22
|
+
*
|
|
23
|
+
* The wiring file side-effect imports the user's auth file (so `pikkuBetterAuth`
|
|
24
|
+
* registers its factory), builds ONE shared sessionless handler that reads the
|
|
25
|
+
* resolved `services.auth` and delegates to better-auth's fetch handler, wires a
|
|
26
|
+
* catch-all `${basePath}{/*splat}` route per method to it, and registers the
|
|
27
|
+
* better-auth session-bridge middleware globally. Provider/plugin metadata for
|
|
28
|
+
* the console is emitted separately as `auth-meta.gen.json` (see
|
|
29
|
+
* serializeAuthMeta). Because this is normal, statically inspectable HTTP
|
|
30
|
+
* wiring, the routes flow through inspection into the deploy manifest (one
|
|
31
|
+
* worker for all auth routes).
|
|
32
|
+
*/
|
|
33
|
+
export declare const serializeAuthGen: (definition: AuthDefinition, providers: string[], authFile: string, typesDeclarationFile: string, packageMappings: Record<string, string>) => AuthGenOutput;
|