diff --git a/bun.lockb b/bun.lockb index ca38100..47d7795 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0009_strong_firebird.sql b/drizzle/0009_strong_firebird.sql new file mode 100644 index 0000000..d54deaa --- /dev/null +++ b/drizzle/0009_strong_firebird.sql @@ -0,0 +1,18 @@ +CREATE TABLE `passkeys` ( + `id` text PRIMARY KEY NOT NULL, + `public_key` blob NOT NULL, + `user_id` text NOT NULL, + `webauthn_user_id` text NOT NULL, + `counter` blob NOT NULL, + `is_backed_up` integer NOT NULL, + `device_type` text NOT NULL, + `transports` text, + `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + `last_used_at` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `passkeys_webauthn_user_id_unique` ON `passkeys` (`webauthn_user_id`);--> statement-breakpoint +CREATE INDEX `user_id_id_idx` ON `passkeys` (`user_id`,`id`);--> statement-breakpoint +CREATE INDEX `webauthn_user_id_id_idx` ON `passkeys` (`webauthn_user_id`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_id_webauthn_user_id_uk` ON `passkeys` (`user_id`,`webauthn_user_id`); \ No newline at end of file diff --git a/drizzle/0010_pretty_genesis.sql b/drizzle/0010_pretty_genesis.sql new file mode 100644 index 0000000..37b72c0 --- /dev/null +++ b/drizzle/0010_pretty_genesis.sql @@ -0,0 +1,23 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_passkeys` ( + `id` text PRIMARY KEY NOT NULL, + `public_key` blob NOT NULL, + `user_id` text NOT NULL, + `webauthn_user_id` text NOT NULL, + `counter` integer NOT NULL, + `is_backed_up` integer NOT NULL, + `device_type` text NOT NULL, + `transports` text, + `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + `last_used_at` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_passkeys`("id", "public_key", "user_id", "webauthn_user_id", "counter", "is_backed_up", "device_type", "transports", "created_at", "last_used_at") SELECT "id", "public_key", "user_id", "webauthn_user_id", "counter", "is_backed_up", "device_type", "transports", "created_at", "last_used_at" FROM `passkeys`;--> statement-breakpoint +DROP TABLE `passkeys`;--> statement-breakpoint +ALTER TABLE `__new_passkeys` RENAME TO `passkeys`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `passkeys_webauthn_user_id_unique` ON `passkeys` (`webauthn_user_id`);--> statement-breakpoint +CREATE INDEX `user_id_id_idx` ON `passkeys` (`user_id`,`id`);--> statement-breakpoint +CREATE INDEX `webauthn_user_id_id_idx` ON `passkeys` (`webauthn_user_id`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_id_webauthn_user_id_uk` ON `passkeys` (`user_id`,`webauthn_user_id`); \ No newline at end of file diff --git a/drizzle/0011_busy_green_goblin.sql b/drizzle/0011_busy_green_goblin.sql new file mode 100644 index 0000000..57dd6e5 --- /dev/null +++ b/drizzle/0011_busy_green_goblin.sql @@ -0,0 +1,23 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_passkeys` ( + `id` text PRIMARY KEY NOT NULL, + `public_key` text NOT NULL, + `user_id` text NOT NULL, + `webauthn_user_id` text NOT NULL, + `counter` integer NOT NULL, + `is_backed_up` integer NOT NULL, + `device_type` text NOT NULL, + `transports` text, + `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + `last_used_at` text, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_passkeys`("id", "public_key", "user_id", "webauthn_user_id", "counter", "is_backed_up", "device_type", "transports", "created_at", "last_used_at") SELECT "id", "public_key", "user_id", "webauthn_user_id", "counter", "is_backed_up", "device_type", "transports", "created_at", "last_used_at" FROM `passkeys`;--> statement-breakpoint +DROP TABLE `passkeys`;--> statement-breakpoint +ALTER TABLE `__new_passkeys` RENAME TO `passkeys`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `passkeys_webauthn_user_id_unique` ON `passkeys` (`webauthn_user_id`);--> statement-breakpoint +CREATE INDEX `user_id_id_idx` ON `passkeys` (`user_id`,`id`);--> statement-breakpoint +CREATE INDEX `webauthn_user_id_id_idx` ON `passkeys` (`webauthn_user_id`,`id`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_id_webauthn_user_id_uk` ON `passkeys` (`user_id`,`webauthn_user_id`); \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..d6347e1 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,287 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "53c3c185-40b1-4ec0-89f7-008a03d2f001", + "prevId": "cee9c076-dfea-427d-b7d3-be89264ade6a", + "tables": { + "fragments": { + "name": "fragments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scrap_id": { + "name": "scrap_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "fragments_scrap_id_scraps_id_fk": { + "name": "fragments_scrap_id_scraps_id_fk", + "tableFrom": "fragments", + "tableTo": "scraps", + "columnsFrom": [ + "scrap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fragments_author_id_users_id_fk": { + "name": "fragments_author_id_users_id_fk", + "tableFrom": "fragments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "passkeys": { + "name": "passkeys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "webauthn_user_id": { + "name": "webauthn_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_backed_up": { + "name": "is_backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "passkeys_webauthn_user_id_unique": { + "name": "passkeys_webauthn_user_id_unique", + "columns": [ + "webauthn_user_id" + ], + "isUnique": true + }, + "user_id_id_idx": { + "name": "user_id_id_idx", + "columns": [ + "user_id", + "id" + ], + "isUnique": false + }, + "webauthn_user_id_id_idx": { + "name": "webauthn_user_id_id_idx", + "columns": [ + "webauthn_user_id", + "id" + ], + "isUnique": false + }, + "user_id_webauthn_user_id_uk": { + "name": "user_id_webauthn_user_id_uk", + "columns": [ + "user_id", + "webauthn_user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "passkeys_user_id_users_id_fk": { + "name": "passkeys_user_id_users_id_fk", + "tableFrom": "passkeys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scraps": { + "name": "scraps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scraps_owner_id_users_id_fk": { + "name": "scraps_owner_id_users_id_fk", + "tableFrom": "scraps", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..10fc861 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,287 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6d0cbc30-40ff-4f84-816a-15f8f60ba37b", + "prevId": "53c3c185-40b1-4ec0-89f7-008a03d2f001", + "tables": { + "fragments": { + "name": "fragments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scrap_id": { + "name": "scrap_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "fragments_scrap_id_scraps_id_fk": { + "name": "fragments_scrap_id_scraps_id_fk", + "tableFrom": "fragments", + "tableTo": "scraps", + "columnsFrom": [ + "scrap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fragments_author_id_users_id_fk": { + "name": "fragments_author_id_users_id_fk", + "tableFrom": "fragments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "passkeys": { + "name": "passkeys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "webauthn_user_id": { + "name": "webauthn_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_backed_up": { + "name": "is_backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "passkeys_webauthn_user_id_unique": { + "name": "passkeys_webauthn_user_id_unique", + "columns": [ + "webauthn_user_id" + ], + "isUnique": true + }, + "user_id_id_idx": { + "name": "user_id_id_idx", + "columns": [ + "user_id", + "id" + ], + "isUnique": false + }, + "webauthn_user_id_id_idx": { + "name": "webauthn_user_id_id_idx", + "columns": [ + "webauthn_user_id", + "id" + ], + "isUnique": false + }, + "user_id_webauthn_user_id_uk": { + "name": "user_id_webauthn_user_id_uk", + "columns": [ + "user_id", + "webauthn_user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "passkeys_user_id_users_id_fk": { + "name": "passkeys_user_id_users_id_fk", + "tableFrom": "passkeys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scraps": { + "name": "scraps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scraps_owner_id_users_id_fk": { + "name": "scraps_owner_id_users_id_fk", + "tableFrom": "scraps", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..ae22732 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,287 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2121a8eb-37ec-4235-9d51-9417b691e5f9", + "prevId": "6d0cbc30-40ff-4f84-816a-15f8f60ba37b", + "tables": { + "fragments": { + "name": "fragments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scrap_id": { + "name": "scrap_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "fragments_scrap_id_scraps_id_fk": { + "name": "fragments_scrap_id_scraps_id_fk", + "tableFrom": "fragments", + "tableTo": "scraps", + "columnsFrom": [ + "scrap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fragments_author_id_users_id_fk": { + "name": "fragments_author_id_users_id_fk", + "tableFrom": "fragments", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "passkeys": { + "name": "passkeys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "webauthn_user_id": { + "name": "webauthn_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_backed_up": { + "name": "is_backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "passkeys_webauthn_user_id_unique": { + "name": "passkeys_webauthn_user_id_unique", + "columns": [ + "webauthn_user_id" + ], + "isUnique": true + }, + "user_id_id_idx": { + "name": "user_id_id_idx", + "columns": [ + "user_id", + "id" + ], + "isUnique": false + }, + "webauthn_user_id_id_idx": { + "name": "webauthn_user_id_id_idx", + "columns": [ + "webauthn_user_id", + "id" + ], + "isUnique": false + }, + "user_id_webauthn_user_id_uk": { + "name": "user_id_webauthn_user_id_uk", + "columns": [ + "user_id", + "webauthn_user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "passkeys_user_id_users_id_fk": { + "name": "passkeys_user_id_users_id_fk", + "tableFrom": "passkeys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scraps": { + "name": "scraps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scraps_owner_id_users_id_fk": { + "name": "scraps_owner_id_users_id_fk", + "tableFrom": "scraps", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d4d0033..f81c27c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -64,6 +64,27 @@ "when": 1733418611394, "tag": "0008_lean_spencer_smythe", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1733440947220, + "tag": "0009_strong_firebird", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1734080866032, + "tag": "0010_pretty_genesis", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1734100559451, + "tag": "0011_busy_green_goblin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 8671db7..8c06d8d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@mantine/form": "^7.14.3", "@mantine/hooks": "^7.14.3", "@mantine/notifications": "^7.14.3", + "@simplewebauthn/browser": "^13.0.0", + "@simplewebauthn/server": "^12.0.0", "@tabler/icons-react": "^3.21.0", "@tanstack/react-router": "^1.77.5", "bcryptjs": "^2.4.3", @@ -42,13 +44,14 @@ "@cloudflare/workers-types": "^4.20240529.0", "@hono/vite-build": "^1.1.0", "@hono/vite-dev-server": "^0.17.0", + "@simplewebauthn/types": "^12.0.0", "@tanstack/router-devtools": "^1.77.5", "@tanstack/router-plugin": "^1.76.4", "@tsconfig/strictest": "^2.0.5", "@types/bcryptjs": "^2.4.6", "@types/bun": "^1.1.14", "@types/mdast": "^4.0.4", - "@types/node": "^22.7.7", + "@types/node": "^22.10.2", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "autoprefixer": "^10.4.20", diff --git a/src/server/constant/passkey.ts b/src/server/constant/passkey.ts new file mode 100644 index 0000000..4a4021f --- /dev/null +++ b/src/server/constant/passkey.ts @@ -0,0 +1,4 @@ +export const PASSKEY_REGISTRATION_SESSION_TTL = 60 * 5 // 5 minutes +export const PASSKEY_AUTHENTICATION_CHALLENGE_TTL = 60 * 5 // 5 minutes +export const PASSKEY_AUTHENTICATION_CHALLENGE_COOKIE_NAME = + 'authentication_challenge' diff --git a/src/server/db/customType.ts b/src/server/db/customType.ts new file mode 100644 index 0000000..35b0bd9 --- /dev/null +++ b/src/server/db/customType.ts @@ -0,0 +1,10 @@ +import { customType } from 'drizzle-orm/sqlite-core' + +export const uint8ArrayAsBase64 = customType<{ + data: Uint8Array + driverData: string +}>({ + dataType: () => 'text', + toDriver: (value) => btoa(String.fromCharCode(...value)), + fromDriver: (value) => Uint8Array.from(Buffer.from(value, 'base64')), +}) diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 1d55190..31667ee 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -1,7 +1,13 @@ import type { FragmentId } from '@/common/model/fragment' import type { UserId } from '@/common/model/user' +import { uint8ArrayAsBase64 } from '@/server/db/customType' +import type { + AuthenticatorTransportFuture, + Base64URLString, + CredentialDeviceType, +} from '@simplewebauthn/types' import { relations, sql } from 'drizzle-orm' -import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { index, int, sqliteTable, text, unique } from 'drizzle-orm/sqlite-core' export const fragments = sqliteTable('fragments', { id: int().$type().primaryKey({ autoIncrement: true }), @@ -50,4 +56,52 @@ export const users = sqliteTable('users', { }) export const usersRelations = relations(users, ({ many }) => ({ scraps: many(scraps), + passkeys: many(passkeys), +})) + +// refs: https://simplewebauthn.dev/docs/packages/server#additional-data-structures +export const passkeys = sqliteTable( + 'passkeys', + { + id: text().$type().primaryKey(), + publicKey: uint8ArrayAsBase64('public_key').notNull(), + userId: text('user_id') + .$type() + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + webauthnUserId: text('webauthn_user_id') + .$type() + .notNull() + .unique(), + counter: int().notNull(), + isBackedUp: int('is_backed_up', { mode: 'boolean' }).notNull(), + deviceType: text('device_type', { + enum: ['singleDevice', 'multiDevice'], + }) + .$type() + .notNull(), + transports: text('transports', { mode: 'json' }).$type< + AuthenticatorTransportFuture[] + >(), + createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`).notNull(), + // 最終ログイン日時 + lastUsedAt: text('last_used_at'), + }, + (table) => ({ + userIdWebauthnUserIdUk: unique('user_id_webauthn_user_id_uk').on( + table.userId, + table.webauthnUserId, + ), + userIdIdIdx: index('user_id_id_idx').on(table.userId, table.id), + webauthnUserIdIdIdx: index('webauthn_user_id_id_idx').on( + table.webauthnUserId, + table.id, + ), + }), +) +export const passkeysRelations = relations(passkeys, ({ one }) => ({ + user: one(users, { + fields: [passkeys.userId], + references: [users.id], + }), })) diff --git a/src/server/model/passkey.ts b/src/server/model/passkey.ts new file mode 100644 index 0000000..ab3c738 --- /dev/null +++ b/src/server/model/passkey.ts @@ -0,0 +1,17 @@ +import type { User } from '@/common/model/user' +import type { + AuthenticatorTransportFuture, + Base64URLString, + CredentialDeviceType, +} from '@simplewebauthn/types' + +export type Passkey = { + id: Base64URLString + publicKey: Uint8Array + user: User + webauthnUserId: Base64URLString + counter: number + isBackedUp: boolean + deviceType: CredentialDeviceType + transports: AuthenticatorTransportFuture[] | null +} diff --git a/src/server/repository/passkey/authenticationSession.ts b/src/server/repository/passkey/authenticationSession.ts new file mode 100644 index 0000000..922d67d --- /dev/null +++ b/src/server/repository/passkey/authenticationSession.ts @@ -0,0 +1,45 @@ +import type { UserId } from '@/common/model/user' +import { z } from 'zod' + +const authenticationSessionSchema = z.object({ + challenge: z.string(), +}) +export type AuthenticationSession = z.infer + +export interface IPasskeyAuthenticationSessionRepository { + store(userId: UserId, session: AuthenticationSession): Promise + load(userId: UserId): Promise +} + +export class KVPasskeyAuthenticationSessionRepository + implements IPasskeyAuthenticationSessionRepository +{ + constructor( + private kv: KVNamespace, + private ttl?: number, + ) {} + + async store(userId: UserId, session: AuthenticationSession): Promise { + const key = this.formatKey(userId) + const options = this.ttl ? { expirationTtl: this.ttl } : {} + await this.kv.put(key, JSON.stringify(session), options) + } + + async load(userId: UserId): Promise { + const key = this.formatKey(userId) + const raw = await this.kv.get(key) + if (raw === null) { + return null + } + + try { + return authenticationSessionSchema.parse(JSON.parse(raw)) + } catch (e) { + return null + } + } + + private formatKey(userId: UserId) { + return `passkey:authentication:${userId}` + } +} diff --git a/src/server/repository/passkey/index.ts b/src/server/repository/passkey/index.ts new file mode 100644 index 0000000..468ff3e --- /dev/null +++ b/src/server/repository/passkey/index.ts @@ -0,0 +1,76 @@ +import type { UserId } from '@/common/model/user' +import * as schema from '@/server/db/schema' +import type { Passkey } from '@/server/model/passkey' +import { eq, sql } from 'drizzle-orm' +import type { DrizzleD1Database } from 'drizzle-orm/d1' + +export interface IPasskeyRepository { + findByUserId(userId: UserId): Promise + find(credentialId: Passkey['id']): Promise + save(passkey: Passkey): Promise + updateLastUsedAt(credentialId: Passkey['id']): Promise +} + +export class PasskeyRepository implements IPasskeyRepository { + constructor(private db: DrizzleD1Database) {} + + async findByUserId(userId: UserId) { + const user = await this.db.query.users.findFirst({ + where: (users, { eq }) => eq(users.id, userId), + with: { passkeys: true }, + }) + if (!user) return [] + + const passkeys: Passkey[] = user.passkeys.map((p) => ({ + ...p, + user, + })) + return passkeys + } + + async find(credentialId: Passkey['id']) { + const passkey = await this.db.query.passkeys.findFirst({ + where: (passkeys, { eq }) => eq(passkeys.id, credentialId), + with: { user: true }, + }) + if (!passkey) return null + + return { + ...passkey, + } + } + + async save(passkey: Passkey) { + // upsert + await this.db + .insert(schema.passkeys) + .values({ + id: passkey.id, + publicKey: passkey.publicKey, + userId: passkey.user.id, + webauthnUserId: passkey.webauthnUserId, + counter: passkey.counter, + isBackedUp: passkey.isBackedUp, + deviceType: passkey.deviceType, + transports: passkey.transports, + }) + .onConflictDoUpdate({ + target: schema.passkeys.id, + set: { + counter: passkey.counter, + isBackedUp: passkey.isBackedUp, + deviceType: passkey.deviceType, + transports: passkey.transports, + }, + }) + } + + async updateLastUsedAt(credentialId: Passkey['id']): Promise { + await this.db + .update(schema.passkeys) + .set({ + lastUsedAt: sql`CURRENT_TIMESTAMP`, + }) + .where(eq(schema.passkeys.id, credentialId)) + } +} diff --git a/src/server/repository/passkey/registrationSession.ts b/src/server/repository/passkey/registrationSession.ts new file mode 100644 index 0000000..87d1815 --- /dev/null +++ b/src/server/repository/passkey/registrationSession.ts @@ -0,0 +1,49 @@ +import type { UserId } from '@/common/model/user' +import { z } from 'zod' + +const passkeyRegistrationSessionSchema = z.object({ + challenge: z.string(), + webauthnUserId: z.string(), +}) +export type RegistrationSession = z.infer< + typeof passkeyRegistrationSessionSchema +> + +export interface IPasskeyRegistrationSessionRepository { + store(userId: UserId, session: RegistrationSession): Promise + load(userId: UserId): Promise +} + +export class KVPasskeyRegistrationSessionRepository + implements IPasskeyRegistrationSessionRepository +{ + constructor( + private kv: KVNamespace, + private ttl?: number, + ) {} + + async store(userId: UserId, session: RegistrationSession) { + const key = this.formatKey(userId) + const options = this.ttl ? { expirationTtl: this.ttl } : {} + await this.kv.put(key, JSON.stringify(session), options) + } + + async load(userId: UserId) { + const key = this.formatKey(userId) + const raw = await this.kv.get(key) + if (!raw) { + return null + } + + try { + return passkeyRegistrationSessionSchema.parse(JSON.parse(raw)) + } catch (e) { + console.error(e) + return null + } + } + + private formatKey(userId: UserId) { + return `passkey:registration:${userId}` + } +} diff --git a/src/server/repository/user.ts b/src/server/repository/user.ts new file mode 100644 index 0000000..337562c --- /dev/null +++ b/src/server/repository/user.ts @@ -0,0 +1,24 @@ +import type { User, UserId } from '@/common/model/user' +import type * as schema from '@/server/db/schema' +import type { DrizzleD1Database } from 'drizzle-orm/d1' + +export interface IUserRepository { + find(userId: UserId): Promise +} + +export class UserRepository implements IUserRepository { + constructor(private db: DrizzleD1Database) {} + + async find(userId: UserId) { + const rows = await this.db.query.users.findFirst({ + where: (users, { eq }) => eq(users.id, userId), + }) + if (!rows) { + return null + } + + return { + ...rows, + } + } +} diff --git a/src/server/routes/auth/index.ts b/src/server/routes/auth/index.ts new file mode 100644 index 0000000..4950b21 --- /dev/null +++ b/src/server/routes/auth/index.ts @@ -0,0 +1,10 @@ +import passkeyAuth from '@/server/routes/auth/passkey' +import passwordAuth from '@/server/routes/auth/password' +import { Hono } from 'hono' + +const auth = new Hono() + .basePath('/auth') + .route('/', passwordAuth) + .route('/passkey', passkeyAuth) + +export default auth diff --git a/src/server/routes/auth/passkey.ts b/src/server/routes/auth/passkey.ts new file mode 100644 index 0000000..a9f167a --- /dev/null +++ b/src/server/routes/auth/passkey.ts @@ -0,0 +1,159 @@ +import type { User } from '@/common/model/user' +import { + PASSKEY_AUTHENTICATION_CHALLENGE_COOKIE_NAME, + PASSKEY_AUTHENTICATION_CHALLENGE_TTL, + PASSKEY_REGISTRATION_SESSION_TTL, +} from '@/server/constant/passkey' +import { SESSION_COOKIE_NAME, SESSION_TTL } from '@/server/constant/session' +import type { AppEnv } from '@/server/env' +import { sessionAuthMiddleware } from '@/server/middleware/sessionAuth' +import { PasskeyRepository } from '@/server/repository/passkey' +import { KVPasskeyRegistrationSessionRepository } from '@/server/repository/passkey/registrationSession' +import { UserRepository } from '@/server/repository/user' +import { + type IPasskeyAuthenticationService, + PasskeyAuthenticationService, +} from '@/server/service/passkey/authentication' +import { + type IPasskeyRegistrationService, + PasskeyRegistrationService, +} from '@/server/service/passkey/registration' +import { honoFactory } from '@/server/utility/factory' +import type { + AuthenticationResponseJSON, + RegistrationResponseJSON, +} from '@simplewebauthn/types' +import { getCookie, setCookie } from 'hono/cookie' +import { createMiddleware } from 'hono/factory' +import { HTTPException } from 'hono/http-exception' +import { validator } from 'hono/validator' + +type AppEnvWithDeps = AppEnv & { + Variables: { + passkeyRegistrationService: IPasskeyRegistrationService + passkeyAuthenticationService: IPasskeyAuthenticationService + } +} + +const injectDeps = createMiddleware(async (c, next) => { + const userRepo = new UserRepository(c.var.db) + const passkeyRepo = new PasskeyRepository(c.var.db) + const regSessionRepo = new KVPasskeyRegistrationSessionRepository( + c.env.SESSION_KV, + PASSKEY_REGISTRATION_SESSION_TTL, + ) + + const regService = new PasskeyRegistrationService( + userRepo, + passkeyRepo, + regSessionRepo, + ) + const authService = new PasskeyAuthenticationService(passkeyRepo) + + c.set('passkeyRegistrationService', regService) + c.set('passkeyAuthenticationService', authService) + + await next() +}) + +const passkeyAuth = honoFactory + .createApp() + .use(injectDeps) + .get('/attestation/options', sessionAuthMiddleware, async (c) => { + const session = c.var.session + + const options = await c.var.passkeyRegistrationService.generateOptions({ + userId: session.userId, + }) + return c.json(options) + }) + .post( + '/attestation', + sessionAuthMiddleware, + // body が必要なことだけ明示する + validator('json', (value) => value), + async (c) => { + const session = c.var.session + const body = await c.req.json() + + let verified: boolean + try { + const verificationJSON = await c.var.passkeyRegistrationService.verify({ + userId: session.userId, + registrationResponse: body, + }) + verified = verificationJSON.verified + } catch (e) { + console.error(e) + throw new HTTPException(400) + } + + if (!verified) { + throw new HTTPException(400) + } + + return c.json({ verified }, 201) + }, + ) + .get('/assertion/options', async (c) => { + const options = await c.var.passkeyAuthenticationService.generateOptions({}) + + setCookie( + c, + PASSKEY_AUTHENTICATION_CHALLENGE_COOKIE_NAME, + options.challenge, + { + httpOnly: true, + secure: import.meta.env.PROD, + sameSite: 'strict', + maxAge: PASSKEY_AUTHENTICATION_CHALLENGE_TTL, + }, + ) + + return c.json(options) + }) + .post( + '/assertion', + // body が必要なことだけ明示する + validator('json', (value) => value), + async (c) => { + const expectedChallenge = getCookie( + c, + PASSKEY_AUTHENTICATION_CHALLENGE_COOKIE_NAME, + ) + if (!expectedChallenge) { + throw new HTTPException(400) + } + const body = await c.req.json() + + let verified: boolean + let user: User + try { + const result = await c.var.passkeyAuthenticationService.verify({ + authenticationResponse: body, + expectedChallenge, + }) + verified = result.verified + user = result.user + } catch (e) { + console.error(e) + throw new HTTPException(400) + } + + if (!verified) { + throw new HTTPException(400) + } + + const session = await c.var.sessionRepository.createSession(user.id) + setCookie(c, SESSION_COOKIE_NAME, session.id, { + httpOnly: true, + sameSite: 'strict', + secure: import.meta.env.PROD, + maxAge: SESSION_TTL, + }) + + return c.json({ verified }, 200) + }, + ) + +export default passkeyAuth diff --git a/src/server/routes/auth.ts b/src/server/routes/auth/password.ts similarity index 97% rename from src/server/routes/auth.ts rename to src/server/routes/auth/password.ts index 43bb3f9..dca4a70 100644 --- a/src/server/routes/auth.ts +++ b/src/server/routes/auth/password.ts @@ -17,9 +17,8 @@ import { z } from 'zod' const DUMMY_PASSWORD_HASH = '$2a$10$jl8KgQv7CRjy2K5rhoiLmOf6Xa4UTltGzdbn6vYDWGQlSuzXT4CpK' -const auth = honoFactory +const passwordAuth = honoFactory .createApp() - .basePath('/auth') .post( '/login', zValidator( @@ -98,4 +97,4 @@ const auth = honoFactory return c.body(null, 204) }) -export default auth +export default passwordAuth diff --git a/src/server/service/passkey/authentication.ts b/src/server/service/passkey/authentication.ts new file mode 100644 index 0000000..5ba09d7 --- /dev/null +++ b/src/server/service/passkey/authentication.ts @@ -0,0 +1,77 @@ +import type { User } from '@/common/model/user' +import type { IPasskeyRepository } from '@/server/repository/passkey' +import { origin, rpId } from '@/server/service/passkey/rp' +import { + generateAuthenticationOptions, + verifyAuthenticationResponse, +} from '@simplewebauthn/server' +import type { + AuthenticationResponseJSON, + PublicKeyCredentialRequestOptionsJSON, +} from '@simplewebauthn/types' + +export type GenerateOptionsInput = Record +export type VerifyInput = { + authenticationResponse: AuthenticationResponseJSON + expectedChallenge: string +} + +export type VerifyOutput = { + verified: boolean + user: User +} + +export type IPasskeyAuthenticationService = { + generateOptions( + input: GenerateOptionsInput, + ): Promise + verify(input: VerifyInput): Promise +} + +export class PasskeyAuthenticationService + implements IPasskeyAuthenticationService +{ + constructor(private passkeyRepo: IPasskeyRepository) {} + + async generateOptions( + _input: GenerateOptionsInput, + ): Promise { + return await generateAuthenticationOptions({ + rpID: rpId, + userVerification: 'preferred', + allowCredentials: [], + }) + } + + async verify(input: VerifyInput): Promise { + const passkey = await this.passkeyRepo.find(input.authenticationResponse.id) + if (!passkey) { + throw new Error('Passkey not found') + } + + const verification = await verifyAuthenticationResponse({ + response: input.authenticationResponse, + expectedChallenge: input.expectedChallenge, + expectedOrigin: origin, + expectedRPID: rpId, + credential: { + id: passkey.id, + publicKey: passkey.publicKey, + counter: passkey.counter, + transports: passkey.transports ?? undefined, + }, + requireUserVerification: false, + }) + + const { verified } = verification + if (verified) { + // biome-ignore lint/style/noNonNullAssertion: + const { newCounter } = verification.authenticationInfo! + passkey.counter = newCounter + await this.passkeyRepo.save(passkey) + await this.passkeyRepo.updateLastUsedAt(passkey.id) + } + + return { verified, user: passkey.user } + } +} diff --git a/src/server/service/passkey/registration.ts b/src/server/service/passkey/registration.ts new file mode 100644 index 0000000..d7e2afc --- /dev/null +++ b/src/server/service/passkey/registration.ts @@ -0,0 +1,118 @@ +import type { UserId } from '@/common/model/user' +import type { Passkey } from '@/server/model/passkey' +import type { IPasskeyRepository } from '@/server/repository/passkey' +import type { IPasskeyRegistrationSessionRepository } from '@/server/repository/passkey/registrationSession' +import type { IUserRepository } from '@/server/repository/user' +import { origin, rpId, rpName } from '@/server/service/passkey/rp' +import { + type VerifiedRegistrationResponse, + generateRegistrationOptions, + verifyRegistrationResponse, +} from '@simplewebauthn/server' +import type { + PublicKeyCredentialCreationOptionsJSON, + RegistrationResponseJSON, +} from '@simplewebauthn/types' + +export type GenerateOptionsInput = { + userId: UserId +} + +export type VerifyInput = { + userId: UserId + registrationResponse: RegistrationResponseJSON +} + +export interface IPasskeyRegistrationService { + generateOptions( + input: GenerateOptionsInput, + ): Promise + verify( + input: VerifyInput, + ): Promise> +} + +export class PasskeyRegistrationService implements IPasskeyRegistrationService { + constructor( + private userRepo: IUserRepository, + private passkeyRepo: IPasskeyRepository, + private passkeyRegistrationSessionRepo: IPasskeyRegistrationSessionRepository, + ) {} + + // refs: https://simplewebauthn.dev/docs/packages/server#1-generate-registration-options + async generateOptions(input: GenerateOptionsInput) { + const user = await this.userRepo.find(input.userId) + if (!user) { + throw new Error('User not found') + } + const userPasskeys = await this.passkeyRepo.findByUserId(user.id) + + const options: PublicKeyCredentialCreationOptionsJSON = + await generateRegistrationOptions({ + rpName, + rpID: rpId, + userName: user.id, + // Don't prompt users for additional information about the authenticator + attestationType: 'none', + excludeCredentials: userPasskeys.map((passkey) => ({ + id: passkey.id, + ...(passkey.transports && { transports: passkey.transports }), + })), + authenticatorSelection: { + residentKey: 'required', + userVerification: 'preferred', + authenticatorAttachment: 'platform', + }, + }) + + await this.passkeyRegistrationSessionRepo.store(user.id, { + challenge: options.challenge, + webauthnUserId: options.user.id, + }) + + return options + } + + // refs: https://simplewebauthn.dev/docs/packages/server#2-verify-registration-response + async verify(input: VerifyInput) { + const user = await this.userRepo.find(input.userId) + if (!user) { + throw new Error('User not found') + } + const registrationSession = await this.passkeyRegistrationSessionRepo.load( + user.id, + ) + if (!registrationSession) { + throw new Error('Registration session not found') + } + + const verification = await verifyRegistrationResponse({ + response: input.registrationResponse, + expectedChallenge: registrationSession.challenge, + expectedOrigin: origin, + expectedRPID: rpId, + requireUserVerification: false, + }) + + const { verified } = verification + if (verified) { + const { credential, credentialDeviceType, credentialBackedUp } = + // biome-ignore lint/style/noNonNullAssertion: + verification.registrationInfo! + + const newPasskey: Passkey = { + id: credential.id, + publicKey: credential.publicKey, + user: user, + webauthnUserId: registrationSession.webauthnUserId, + counter: credential.counter, + isBackedUp: credentialBackedUp, + deviceType: credentialDeviceType, + transports: credential.transports ?? null, + } + await this.passkeyRepo.save(newPasskey) + } + + return { verified } + } +} diff --git a/src/server/service/passkey/rp.ts b/src/server/service/passkey/rp.ts new file mode 100644 index 0000000..d7875fb --- /dev/null +++ b/src/server/service/passkey/rp.ts @@ -0,0 +1,14 @@ +export const rpName = 'Scrap' +export const origin = + (import.meta.env.PROD && import.meta.env.CF_PAGES_URL) || + 'http://localhost:5173' +export const rpId = createRpId(new URL(origin).hostname) + +function createRpId(hostname: string): string { + if (hostname.endsWith('pages.dev')) { + // branch-name.project-name.pages.dev -> project-name.pages.dev + return hostname.split('.').slice(1).join('.') + } + // それ以外 = 開発環境ではそのまま + return hostname +} diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index ff904ce..a02f3cf 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,5 +1,6 @@ { "extends": ["../../tsconfig.base.json"], + "include": ["**/*", "../../vite-env.d.ts"], "compilerOptions": { "lib": ["ESNext"], "types": ["@cloudflare/workers-types", "vite/client", "node"], diff --git a/vite-env.d.ts b/vite-env.d.ts new file mode 100644 index 0000000..627828b --- /dev/null +++ b/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly CF_PAGES_URL?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/vite.config.ts b/vite.config.ts index 0cf1698..20dd4b4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,7 +62,10 @@ export default defineConfig(({ mode, command }) => { } } + console.log(`CF_PAGES_URL: ${process.env.CF_PAGES_URL}`) + return { + envPrefix: ['VITE_', 'CF_'], ssr: { external: ['react', 'react-dom'], }, diff --git a/wrangler.toml b/wrangler.toml index bb31097..954ea32 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,6 +6,10 @@ compatibility_flags = [ "nodejs_compat" ] [vars] BUN_VERSION = '1.1.38' +[env.production.vars] +BUN_VERSION = '1.1.38' +CF_PAGES_URL = "https://scrap.smatsuo.dev" + [[kv_namespaces]] binding = "SESSION_KV" id = "c05d577e52254456931cef1e83fb0cf4"