plotlink-ows 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/db.ts +8 -0
  4. package/app/lib/llm-client.ts +265 -0
  5. package/app/lib/paths.ts +11 -0
  6. package/app/lib/publish.ts +204 -0
  7. package/app/lib/writer-prompt.ts +44 -0
  8. package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
  9. package/app/node_modules/.prisma/local-client/client.js +5 -0
  10. package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
  11. package/app/node_modules/.prisma/local-client/default.js +5 -0
  12. package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
  13. package/app/node_modules/.prisma/local-client/edge.js +184 -0
  14. package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
  15. package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
  16. package/app/node_modules/.prisma/local-client/index.js +207 -0
  17. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  18. package/app/node_modules/.prisma/local-client/package.json +183 -0
  19. package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
  20. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  21. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
  22. package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
  23. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
  24. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
  25. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
  26. package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
  27. package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
  28. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
  29. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
  30. package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
  31. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
  32. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
  33. package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
  34. package/app/node_modules/.prisma/local-client/wasm.js +191 -0
  35. package/app/prisma/schema.prisma +57 -0
  36. package/app/routes/auth.ts +173 -0
  37. package/app/routes/chat.ts +135 -0
  38. package/app/routes/config.ts +210 -0
  39. package/app/routes/dashboard.ts +186 -0
  40. package/app/routes/oauth.ts +150 -0
  41. package/app/routes/publish.ts +112 -0
  42. package/app/routes/wallet.ts +99 -0
  43. package/app/server.ts +154 -0
  44. package/app/vite.config.ts +19 -0
  45. package/app/web/App.tsx +102 -0
  46. package/app/web/components/Chat.tsx +272 -0
  47. package/app/web/components/Dashboard.tsx +222 -0
  48. package/app/web/components/LLMSetup.tsx +291 -0
  49. package/app/web/components/Layout.tsx +235 -0
  50. package/app/web/components/Login.tsx +62 -0
  51. package/app/web/components/Publish.tsx +245 -0
  52. package/app/web/components/Settings.tsx +175 -0
  53. package/app/web/components/Setup.tsx +84 -0
  54. package/app/web/components/WalletCard.tsx +117 -0
  55. package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
  56. package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
  57. package/app/web/dist/index.html +16 -0
  58. package/app/web/index.html +15 -0
  59. package/app/web/main.tsx +10 -0
  60. package/app/web/plotlink-logo.svg +5 -0
  61. package/app/web/styles.css +51 -0
  62. package/bin/plotlink-ows.js +394 -0
  63. package/lib/ows/index.ts +3 -0
  64. package/lib/ows/policy.ts +68 -0
  65. package/lib/ows/types.ts +14 -0
  66. package/lib/ows/wallet.ts +70 -0
  67. package/package.json +79 -0
  68. package/packages/cli/node_modules/commander/LICENSE +22 -0
  69. package/packages/cli/node_modules/commander/Readme.md +1149 -0
  70. package/packages/cli/node_modules/commander/esm.mjs +16 -0
  71. package/packages/cli/node_modules/commander/index.js +24 -0
  72. package/packages/cli/node_modules/commander/lib/argument.js +149 -0
  73. package/packages/cli/node_modules/commander/lib/command.js +2662 -0
  74. package/packages/cli/node_modules/commander/lib/error.js +39 -0
  75. package/packages/cli/node_modules/commander/lib/help.js +709 -0
  76. package/packages/cli/node_modules/commander/lib/option.js +367 -0
  77. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  78. package/packages/cli/node_modules/commander/package-support.json +16 -0
  79. package/packages/cli/node_modules/commander/package.json +82 -0
  80. package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
  81. package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
  82. package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
  83. package/packages/cli/node_modules/resolve-from/index.js +47 -0
  84. package/packages/cli/node_modules/resolve-from/license +9 -0
  85. package/packages/cli/node_modules/resolve-from/package.json +36 -0
  86. package/packages/cli/node_modules/resolve-from/readme.md +72 -0
  87. package/packages/cli/node_modules/tsup/LICENSE +21 -0
  88. package/packages/cli/node_modules/tsup/README.md +75 -0
  89. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
  90. package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
  91. package/packages/cli/node_modules/tsup/assets/package.json +3 -0
  92. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
  93. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
  94. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
  95. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
  96. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
  97. package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
  98. package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
  99. package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
  100. package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
  101. package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
  102. package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
  103. package/packages/cli/node_modules/tsup/package.json +99 -0
  104. package/packages/cli/node_modules/tsup/schema.json +362 -0
  105. package/packages/cli/package.json +35 -0
  106. package/packages/cli/src/commands/agent-register.ts +77 -0
  107. package/packages/cli/src/commands/chain.ts +29 -0
  108. package/packages/cli/src/commands/claim.ts +70 -0
  109. package/packages/cli/src/commands/create.ts +34 -0
  110. package/packages/cli/src/commands/status.ts +201 -0
  111. package/packages/cli/src/config.ts +103 -0
  112. package/packages/cli/src/index.ts +21 -0
  113. package/packages/cli/src/sdk/abi.ts +222 -0
  114. package/packages/cli/src/sdk/client.ts +713 -0
  115. package/packages/cli/src/sdk/constants.ts +56 -0
  116. package/packages/cli/src/sdk/index.ts +46 -0
  117. package/packages/cli/src/sdk/ipfs.ts +88 -0
  118. package/packages/cli/src/sdk.ts +36 -0
  119. package/packages/cli/tsconfig.json +20 -0
  120. package/packages/cli/tsup.config.ts +14 -0
  121. package/public/.well-known/farcaster.json +38 -0
  122. package/public/basescan-icon.svg +4 -0
  123. package/public/embed-image.png +0 -0
  124. package/public/favicon.png +0 -0
  125. package/public/hunt-token.svg +11 -0
  126. package/public/icon-192.png +0 -0
  127. package/public/icon.png +0 -0
  128. package/public/manifest.json +26 -0
  129. package/public/mc-icon-light.svg +12 -0
  130. package/public/og-image.png +0 -0
  131. package/public/plotlink-logo-symbol.svg +5 -0
  132. package/public/plotlink-logo.svg +5 -0
  133. package/public/screenshot-1.png +0 -0
  134. package/public/screenshot-2.png +0 -0
  135. package/public/screenshot-3.png +0 -0
  136. package/public/splash.png +0 -0
  137. package/public/wide-banner.png +0 -0
  138. package/scripts/backfill-trade-prices.ts +97 -0
  139. package/scripts/backfill-usd-rates.ts +220 -0
  140. package/scripts/e2e-verify.ts +1100 -0
  141. package/scripts/ows-smoke-test.ts +37 -0
  142. package/scripts/score-users.mjs +203 -0
@@ -0,0 +1,191 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!!
3
+ /* eslint-disable */
4
+ // biome-ignore-all lint: generated file
5
+
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+
8
+ const {
9
+ PrismaClientKnownRequestError,
10
+ PrismaClientUnknownRequestError,
11
+ PrismaClientRustPanicError,
12
+ PrismaClientInitializationError,
13
+ PrismaClientValidationError,
14
+ getPrismaClient,
15
+ sqltag,
16
+ empty,
17
+ join,
18
+ raw,
19
+ skip,
20
+ Decimal,
21
+ Debug,
22
+ objectEnumValues,
23
+ makeStrictEnum,
24
+ Extensions,
25
+ warnOnce,
26
+ defineDmmfProperty,
27
+ Public,
28
+ getRuntime,
29
+ createParam,
30
+ } = require('./runtime/wasm-engine-edge.js')
31
+
32
+
33
+ const Prisma = {}
34
+
35
+ exports.Prisma = Prisma
36
+ exports.$Enums = {}
37
+
38
+ /**
39
+ * Prisma Client JS version: 6.19.3
40
+ * Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
41
+ */
42
+ Prisma.prismaVersion = {
43
+ client: "6.19.3",
44
+ engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
45
+ }
46
+
47
+ Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
48
+ Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
49
+ Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
50
+ Prisma.PrismaClientInitializationError = PrismaClientInitializationError
51
+ Prisma.PrismaClientValidationError = PrismaClientValidationError
52
+ Prisma.Decimal = Decimal
53
+
54
+ /**
55
+ * Re-export of sql-template-tag
56
+ */
57
+ Prisma.sql = sqltag
58
+ Prisma.empty = empty
59
+ Prisma.join = join
60
+ Prisma.raw = raw
61
+ Prisma.validator = Public.validator
62
+
63
+ /**
64
+ * Extensions
65
+ */
66
+ Prisma.getExtensionContext = Extensions.getExtensionContext
67
+ Prisma.defineExtension = Extensions.defineExtension
68
+
69
+ /**
70
+ * Shorthand utilities for JSON filtering
71
+ */
72
+ Prisma.DbNull = objectEnumValues.instances.DbNull
73
+ Prisma.JsonNull = objectEnumValues.instances.JsonNull
74
+ Prisma.AnyNull = objectEnumValues.instances.AnyNull
75
+
76
+ Prisma.NullTypes = {
77
+ DbNull: objectEnumValues.classes.DbNull,
78
+ JsonNull: objectEnumValues.classes.JsonNull,
79
+ AnyNull: objectEnumValues.classes.AnyNull
80
+ }
81
+
82
+
83
+
84
+
85
+
86
+ /**
87
+ * Enums
88
+ */
89
+ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
90
+ Serializable: 'Serializable'
91
+ });
92
+
93
+ exports.Prisma.SessionScalarFieldEnum = {
94
+ id: 'id',
95
+ token: 'token',
96
+ createdAt: 'createdAt',
97
+ expiresAt: 'expiresAt'
98
+ };
99
+
100
+ exports.Prisma.SettingScalarFieldEnum = {
101
+ key: 'key',
102
+ value: 'value'
103
+ };
104
+
105
+ exports.Prisma.SortOrder = {
106
+ asc: 'asc',
107
+ desc: 'desc'
108
+ };
109
+
110
+
111
+ exports.Prisma.ModelName = {
112
+ Session: 'Session',
113
+ Setting: 'Setting'
114
+ };
115
+ /**
116
+ * Create the Client
117
+ */
118
+ const config = {
119
+ "generator": {
120
+ "name": "client",
121
+ "provider": {
122
+ "fromEnvVar": null,
123
+ "value": "prisma-client-js"
124
+ },
125
+ "output": {
126
+ "value": "/Users/cho/Projects/plotlink-ows/app/node_modules/.prisma/local-client",
127
+ "fromEnvVar": null
128
+ },
129
+ "config": {
130
+ "engineType": "library"
131
+ },
132
+ "binaryTargets": [
133
+ {
134
+ "fromEnvVar": null,
135
+ "value": "darwin-arm64",
136
+ "native": true
137
+ }
138
+ ],
139
+ "previewFeatures": [],
140
+ "sourceFilePath": "/Users/cho/Projects/plotlink-ows/app/prisma/schema.prisma",
141
+ "isCustomOutput": true
142
+ },
143
+ "relativeEnvPaths": {
144
+ "rootEnvPath": null
145
+ },
146
+ "relativePath": "../../../prisma",
147
+ "clientVersion": "6.19.3",
148
+ "engineVersion": "c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
149
+ "datasourceNames": [
150
+ "db"
151
+ ],
152
+ "activeProvider": "sqlite",
153
+ "postinstall": false,
154
+ "inlineDatasources": {
155
+ "db": {
156
+ "url": {
157
+ "fromEnvVar": null,
158
+ "value": "file:../../data/local.db"
159
+ }
160
+ }
161
+ },
162
+ "inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../node_modules/.prisma/local-client\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = \"file:../../data/local.db\"\n}\n\nmodel Session {\n id String @id @default(cuid())\n token String @unique\n createdAt DateTime @default(now())\n expiresAt DateTime\n}\n\nmodel Setting {\n key String @id\n value String\n}\n",
163
+ "inlineSchemaHash": "e31b194b10534203be1d4e09555579ffc3126c3700c5558a06db395e2bdfcdd9",
164
+ "copyEngine": true
165
+ }
166
+ config.dirname = '/'
167
+
168
+ config.runtimeDataModel = JSON.parse("{\"models\":{\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Setting\":{\"fields\":[{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
169
+ defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
170
+ config.engineWasm = {
171
+ getRuntime: async () => require('./query_engine_bg.js'),
172
+ getQueryEngineWasmModule: async () => {
173
+ const loader = (await import('#wasm-engine-loader')).default
174
+ const engine = (await loader).default
175
+ return engine
176
+ }
177
+ }
178
+ config.compilerWasm = undefined
179
+
180
+ config.injectableEdgeEnv = () => ({
181
+ parsed: {}
182
+ })
183
+
184
+ if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined) {
185
+ Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined)
186
+ }
187
+
188
+ const PrismaClient = getPrismaClient(config)
189
+ exports.PrismaClient = PrismaClient
190
+ Object.assign(exports, Prisma)
191
+
@@ -0,0 +1,57 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ output = "../../node_modules/.prisma/local-client"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "sqlite"
8
+ url = "file:../../data/local.db"
9
+ }
10
+
11
+ model Session {
12
+ id String @id @default(cuid())
13
+ token String @unique
14
+ createdAt DateTime @default(now())
15
+ expiresAt DateTime
16
+ }
17
+
18
+ model Setting {
19
+ key String @id
20
+ value String
21
+ }
22
+
23
+ model StorySession {
24
+ id String @id @default(cuid())
25
+ title String @default("Untitled Story")
26
+ genre String?
27
+ status String @default("active") // active, finalized, archived
28
+ createdAt DateTime @default(now())
29
+ updatedAt DateTime @updatedAt
30
+ messages Message[]
31
+ drafts Draft[]
32
+ }
33
+
34
+ model Message {
35
+ id String @id @default(cuid())
36
+ sessionId String
37
+ session StorySession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
38
+ role String // user, assistant, system
39
+ content String
40
+ createdAt DateTime @default(now())
41
+ }
42
+
43
+ model Draft {
44
+ id String @id @default(cuid())
45
+ sessionId String
46
+ session StorySession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
47
+ title String
48
+ content String
49
+ genre String?
50
+ status String @default("draft") // draft, ready, published
51
+ txHash String?
52
+ storylineId Int?
53
+ contentCid String?
54
+ gasCost String? // ETH cost in wei
55
+ createdAt DateTime @default(now())
56
+ updatedAt DateTime @updatedAt
57
+ }
@@ -0,0 +1,173 @@
1
+ import { Hono } from "hono";
2
+ import { createHmac, randomBytes } from "crypto";
3
+ import { db } from "../db";
4
+ import fs from "fs";
5
+ import { ENV_FILE } from "../lib/paths";
6
+
7
+ const envPath = ENV_FILE;
8
+
9
+ const auth = new Hono();
10
+
11
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
12
+
13
+ function hashPassphrase(passphrase: string): string {
14
+ return createHmac("sha256", "plotlink-ows").update(passphrase).digest("hex");
15
+ }
16
+
17
+ function readEnvPassphrase(): string | null {
18
+ // Check process.env first
19
+ if (process.env.OWS_PASSPHRASE) return process.env.OWS_PASSPHRASE;
20
+ // Then read from .env file directly
21
+ try {
22
+ if (fs.existsSync(envPath)) {
23
+ const content = fs.readFileSync(envPath, "utf-8");
24
+ const match = content.match(/^OWS_PASSPHRASE=(.+)$/m);
25
+ if (match) return match[1].trim();
26
+ }
27
+ } catch { /* ignore */ }
28
+ return null;
29
+ }
30
+
31
+ async function getStoredHash(): Promise<string | null> {
32
+ const passphrase = readEnvPassphrase();
33
+ if (passphrase) return hashPassphrase(passphrase);
34
+ // Fallback to DB setting
35
+ const setting = await db.setting.findUnique({ where: { key: "passphrase_hash" } });
36
+ return setting?.value ?? null;
37
+ }
38
+
39
+ /** GET /api/auth/status — check if passphrase is configured (first-run detection) */
40
+ auth.get("/status", async (c) => {
41
+ const hash = await getStoredHash();
42
+ return c.json({ configured: !!hash });
43
+ });
44
+
45
+ /** POST /api/auth/setup — first-run passphrase setup */
46
+ auth.post("/setup", async (c) => {
47
+ const existing = await getStoredHash();
48
+ if (existing) {
49
+ return c.json({ error: "Passphrase already configured" }, 409);
50
+ }
51
+
52
+ const body = await c.req.json<{ passphrase: string }>();
53
+ if (!body.passphrase || body.passphrase.length < 4) {
54
+ return c.json({ error: "Passphrase must be at least 4 characters" }, 400);
55
+ }
56
+
57
+ // Persist passphrase to .env file
58
+ const envLine = `OWS_PASSPHRASE=${body.passphrase}`;
59
+ if (fs.existsSync(envPath)) {
60
+ const content = fs.readFileSync(envPath, "utf-8");
61
+ if (content.includes("OWS_PASSPHRASE=")) {
62
+ fs.writeFileSync(envPath, content.replace(/^OWS_PASSPHRASE=.*$/m, envLine));
63
+ } else {
64
+ fs.appendFileSync(envPath, `\n${envLine}\n`);
65
+ }
66
+ } else {
67
+ fs.writeFileSync(envPath, `${envLine}\n`);
68
+ }
69
+ // Also set in process.env for immediate use
70
+ process.env.OWS_PASSPHRASE = body.passphrase;
71
+
72
+ // Auto-login after setup
73
+ const token = randomBytes(32).toString("hex");
74
+ await db.session.create({
75
+ data: {
76
+ token,
77
+ expiresAt: new Date(Date.now() + SESSION_TTL_MS),
78
+ },
79
+ });
80
+
81
+ return c.json({ token });
82
+ });
83
+
84
+ /** POST /api/auth/login — validate passphrase, return session token */
85
+ auth.post("/login", async (c) => {
86
+ const body = await c.req.json<{ passphrase: string }>();
87
+ if (!body.passphrase) {
88
+ return c.json({ error: "Passphrase required" }, 400);
89
+ }
90
+
91
+ const storedHash = await getStoredHash();
92
+ if (!storedHash) {
93
+ return c.json({ error: "Passphrase not configured. Complete first-run setup." }, 500);
94
+ }
95
+
96
+ const inputHash = hashPassphrase(body.passphrase);
97
+ if (inputHash !== storedHash) {
98
+ return c.json({ error: "Invalid passphrase" }, 401);
99
+ }
100
+
101
+ const token = randomBytes(32).toString("hex");
102
+ await db.session.create({
103
+ data: {
104
+ token,
105
+ expiresAt: new Date(Date.now() + SESSION_TTL_MS),
106
+ },
107
+ });
108
+
109
+ return c.json({ token });
110
+ });
111
+
112
+ /** GET /api/auth/verify — check token validity */
113
+ auth.get("/verify", async (c) => {
114
+ const token = c.req.header("Authorization")?.replace("Bearer ", "");
115
+ if (!token) return c.json({ valid: false }, 401);
116
+
117
+ const session = await db.session.findUnique({ where: { token } });
118
+ if (!session || session.expiresAt < new Date()) {
119
+ if (session) await db.session.delete({ where: { token } });
120
+ return c.json({ valid: false }, 401);
121
+ }
122
+
123
+ return c.json({ valid: true });
124
+ });
125
+
126
+ /** POST /api/auth/reset-passphrase — update passphrase (requires auth) */
127
+ auth.post("/reset-passphrase", async (c) => {
128
+ // Verify session first
129
+ const token = c.req.header("Authorization")?.replace("Bearer ", "");
130
+ if (!token) return c.json({ error: "Unauthorized" }, 401);
131
+ const session = await db.session.findUnique({ where: { token } });
132
+ if (!session || session.expiresAt < new Date()) {
133
+ return c.json({ error: "Unauthorized" }, 401);
134
+ }
135
+
136
+ const body = await c.req.json<{ passphrase: string }>();
137
+ if (!body.passphrase || body.passphrase.length < 4) {
138
+ return c.json({ error: "Passphrase must be at least 4 characters" }, 400);
139
+ }
140
+
141
+ // Update .env file
142
+ const envLine = `OWS_PASSPHRASE=${body.passphrase}`;
143
+ if (fs.existsSync(envPath)) {
144
+ const content = fs.readFileSync(envPath, "utf-8");
145
+ if (content.includes("OWS_PASSPHRASE=")) {
146
+ fs.writeFileSync(envPath, content.replace(/^OWS_PASSPHRASE=.*$/m, envLine));
147
+ } else {
148
+ fs.appendFileSync(envPath, `\n${envLine}\n`);
149
+ }
150
+ } else {
151
+ fs.writeFileSync(envPath, `${envLine}\n`);
152
+ }
153
+ process.env.OWS_PASSPHRASE = body.passphrase;
154
+
155
+ return c.json({ success: true });
156
+ });
157
+
158
+ /** Auth middleware for protected routes */
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hono middleware signature
160
+ export async function requireAuth(c: any, next: () => Promise<void>) {
161
+ const token = c.req.header("Authorization")?.replace("Bearer ", "");
162
+ if (!token) return c.json({ error: "Unauthorized" }, 401);
163
+
164
+ const session = await db.session.findUnique({ where: { token } });
165
+ if (!session || session.expiresAt < new Date()) {
166
+ if (session) await db.session.delete({ where: { token } });
167
+ return c.json({ error: "Session expired" }, 401);
168
+ }
169
+
170
+ return next();
171
+ }
172
+
173
+ export { auth as authRoutes };
@@ -0,0 +1,135 @@
1
+ import { Hono } from "hono";
2
+ import { streamSSE } from "hono/streaming";
3
+ import { db } from "../db";
4
+ import { streamChat, type ChatMessage } from "../lib/llm-client";
5
+ import { WRITER_SYSTEM_PROMPT } from "../lib/writer-prompt";
6
+
7
+ const chat = new Hono();
8
+
9
+ /** POST /api/chat/sessions — create a new story session */
10
+ chat.post("/sessions", async (c) => {
11
+ const body = await c.req.json<{ title?: string; genre?: string }>();
12
+ const session = await db.storySession.create({
13
+ data: { title: body.title || "Untitled Story", genre: body.genre || null },
14
+ });
15
+ return c.json(session);
16
+ });
17
+
18
+ /** GET /api/chat/sessions — list all sessions */
19
+ chat.get("/sessions", async (c) => {
20
+ const sessions = await db.storySession.findMany({
21
+ orderBy: { updatedAt: "desc" },
22
+ include: { _count: { select: { messages: true, drafts: true } } },
23
+ });
24
+ return c.json(sessions);
25
+ });
26
+
27
+ /** GET /api/chat/sessions/:id — get session with messages */
28
+ chat.get("/sessions/:id", async (c) => {
29
+ const id = c.req.param("id");
30
+ const session = await db.storySession.findUnique({
31
+ where: { id },
32
+ include: { messages: { orderBy: { createdAt: "asc" } }, drafts: { orderBy: { createdAt: "desc" } } },
33
+ });
34
+ if (!session) return c.json({ error: "Session not found" }, 404);
35
+ return c.json(session);
36
+ });
37
+
38
+ /** DELETE /api/chat/sessions/:id — delete a session */
39
+ chat.delete("/sessions/:id", async (c) => {
40
+ const id = c.req.param("id");
41
+ await db.storySession.delete({ where: { id } });
42
+ return c.json({ success: true });
43
+ });
44
+
45
+ /** POST /api/chat/sessions/:id/send — send a message and stream AI response */
46
+ chat.post("/sessions/:id/send", async (c) => {
47
+ const id = c.req.param("id");
48
+ const body = await c.req.json<{ content: string }>();
49
+
50
+ if (!body.content?.trim()) {
51
+ return c.json({ error: "Message content required" }, 400);
52
+ }
53
+
54
+ // Save user message
55
+ await db.message.create({
56
+ data: { sessionId: id, role: "user", content: body.content },
57
+ });
58
+
59
+ // Build context from conversation history
60
+ const messages = await db.message.findMany({
61
+ where: { sessionId: id },
62
+ orderBy: { createdAt: "asc" },
63
+ });
64
+
65
+ const chatMessages: ChatMessage[] = [
66
+ { role: "system", content: WRITER_SYSTEM_PROMPT },
67
+ ...messages.map((m) => ({ role: m.role as ChatMessage["role"], content: m.content })),
68
+ ];
69
+
70
+ // Stream response via SSE
71
+ return streamSSE(c, async (stream) => {
72
+ let fullResponse = "";
73
+
74
+ try {
75
+ for await (const chunk of streamChat(chatMessages)) {
76
+ fullResponse += chunk;
77
+ await stream.writeSSE({ data: JSON.stringify({ type: "chunk", content: chunk }) });
78
+ }
79
+
80
+ // Save assistant message
81
+ await db.message.create({
82
+ data: { sessionId: id, role: "assistant", content: fullResponse },
83
+ });
84
+
85
+ // Update session title from first exchange if still "Untitled Story"
86
+ const session = await db.storySession.findUnique({ where: { id } });
87
+ if (session?.title === "Untitled Story" && messages.length <= 2) {
88
+ const title = body.content.slice(0, 60) + (body.content.length > 60 ? "..." : "");
89
+ await db.storySession.update({ where: { id }, data: { title } });
90
+ }
91
+
92
+ await stream.writeSSE({ data: JSON.stringify({ type: "done", messageId: fullResponse.slice(0, 20) }) });
93
+ } catch (err: unknown) {
94
+ const message = err instanceof Error ? err.message : "Stream error";
95
+ await stream.writeSSE({ data: JSON.stringify({ type: "error", message }) });
96
+ }
97
+ });
98
+ });
99
+
100
+ /** POST /api/chat/sessions/:id/finalize — create a draft from conversation */
101
+ chat.post("/sessions/:id/finalize", async (c) => {
102
+ const id = c.req.param("id");
103
+ const body = await c.req.json<{ title: string; content: string; genre?: string }>();
104
+
105
+ if (!body.title || !body.content) {
106
+ return c.json({ error: "Title and content required" }, 400);
107
+ }
108
+
109
+ const draft = await db.draft.create({
110
+ data: {
111
+ sessionId: id,
112
+ title: body.title,
113
+ content: body.content,
114
+ genre: body.genre || null,
115
+ status: "ready",
116
+ },
117
+ });
118
+
119
+ await db.storySession.update({
120
+ where: { id },
121
+ data: { status: "finalized" },
122
+ });
123
+
124
+ return c.json(draft);
125
+ });
126
+
127
+ /** GET /api/chat/drafts — list all drafts */
128
+ chat.get("/drafts", async (c) => {
129
+ const drafts = await db.draft.findMany({
130
+ orderBy: { createdAt: "desc" },
131
+ });
132
+ return c.json(drafts);
133
+ });
134
+
135
+ export { chat as chatRoutes };