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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/app/db.ts +8 -0
- package/app/lib/llm-client.ts +265 -0
- package/app/lib/paths.ts +11 -0
- package/app/lib/publish.ts +204 -0
- package/app/lib/writer-prompt.ts +44 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/client.js +5 -0
- package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/default.js +5 -0
- package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/edge.js +184 -0
- package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
- package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
- package/app/node_modules/.prisma/local-client/index.js +207 -0
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +183 -0
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
- package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
- package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
- package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
- package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/wasm.js +191 -0
- package/app/prisma/schema.prisma +57 -0
- package/app/routes/auth.ts +173 -0
- package/app/routes/chat.ts +135 -0
- package/app/routes/config.ts +210 -0
- package/app/routes/dashboard.ts +186 -0
- package/app/routes/oauth.ts +150 -0
- package/app/routes/publish.ts +112 -0
- package/app/routes/wallet.ts +99 -0
- package/app/server.ts +154 -0
- package/app/vite.config.ts +19 -0
- package/app/web/App.tsx +102 -0
- package/app/web/components/Chat.tsx +272 -0
- package/app/web/components/Dashboard.tsx +222 -0
- package/app/web/components/LLMSetup.tsx +291 -0
- package/app/web/components/Layout.tsx +235 -0
- package/app/web/components/Login.tsx +62 -0
- package/app/web/components/Publish.tsx +245 -0
- package/app/web/components/Settings.tsx +175 -0
- package/app/web/components/Setup.tsx +84 -0
- package/app/web/components/WalletCard.tsx +117 -0
- package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
- package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
- package/app/web/dist/index.html +16 -0
- package/app/web/index.html +15 -0
- package/app/web/main.tsx +10 -0
- package/app/web/plotlink-logo.svg +5 -0
- package/app/web/styles.css +51 -0
- package/bin/plotlink-ows.js +394 -0
- package/lib/ows/index.ts +3 -0
- package/lib/ows/policy.ts +68 -0
- package/lib/ows/types.ts +14 -0
- package/lib/ows/wallet.ts +70 -0
- package/package.json +79 -0
- package/packages/cli/node_modules/commander/LICENSE +22 -0
- package/packages/cli/node_modules/commander/Readme.md +1149 -0
- package/packages/cli/node_modules/commander/esm.mjs +16 -0
- package/packages/cli/node_modules/commander/index.js +24 -0
- package/packages/cli/node_modules/commander/lib/argument.js +149 -0
- package/packages/cli/node_modules/commander/lib/command.js +2662 -0
- package/packages/cli/node_modules/commander/lib/error.js +39 -0
- package/packages/cli/node_modules/commander/lib/help.js +709 -0
- package/packages/cli/node_modules/commander/lib/option.js +367 -0
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/packages/cli/node_modules/commander/package-support.json +16 -0
- package/packages/cli/node_modules/commander/package.json +82 -0
- package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
- package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
- package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
- package/packages/cli/node_modules/resolve-from/index.js +47 -0
- package/packages/cli/node_modules/resolve-from/license +9 -0
- package/packages/cli/node_modules/resolve-from/package.json +36 -0
- package/packages/cli/node_modules/resolve-from/readme.md +72 -0
- package/packages/cli/node_modules/tsup/LICENSE +21 -0
- package/packages/cli/node_modules/tsup/README.md +75 -0
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
- package/packages/cli/node_modules/tsup/assets/package.json +3 -0
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
- package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
- package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
- package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
- package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
- package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
- package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
- package/packages/cli/node_modules/tsup/package.json +99 -0
- package/packages/cli/node_modules/tsup/schema.json +362 -0
- package/packages/cli/package.json +35 -0
- package/packages/cli/src/commands/agent-register.ts +77 -0
- package/packages/cli/src/commands/chain.ts +29 -0
- package/packages/cli/src/commands/claim.ts +70 -0
- package/packages/cli/src/commands/create.ts +34 -0
- package/packages/cli/src/commands/status.ts +201 -0
- package/packages/cli/src/config.ts +103 -0
- package/packages/cli/src/index.ts +21 -0
- package/packages/cli/src/sdk/abi.ts +222 -0
- package/packages/cli/src/sdk/client.ts +713 -0
- package/packages/cli/src/sdk/constants.ts +56 -0
- package/packages/cli/src/sdk/index.ts +46 -0
- package/packages/cli/src/sdk/ipfs.ts +88 -0
- package/packages/cli/src/sdk.ts +36 -0
- package/packages/cli/tsconfig.json +20 -0
- package/packages/cli/tsup.config.ts +14 -0
- package/public/.well-known/farcaster.json +38 -0
- package/public/basescan-icon.svg +4 -0
- package/public/embed-image.png +0 -0
- package/public/favicon.png +0 -0
- package/public/hunt-token.svg +11 -0
- package/public/icon-192.png +0 -0
- package/public/icon.png +0 -0
- package/public/manifest.json +26 -0
- package/public/mc-icon-light.svg +12 -0
- package/public/og-image.png +0 -0
- package/public/plotlink-logo-symbol.svg +5 -0
- package/public/plotlink-logo.svg +5 -0
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/public/splash.png +0 -0
- package/public/wide-banner.png +0 -0
- package/scripts/backfill-trade-prices.ts +97 -0
- package/scripts/backfill-usd-rates.ts +220 -0
- package/scripts/e2e-verify.ts +1100 -0
- package/scripts/ows-smoke-test.ts +37 -0
- 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 };
|