@q32/core 0.1.5 → 0.1.7

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/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./mcp.js";
14
14
  export * from "./oauth.js";
15
15
  export * from "./ops-events.js";
16
16
  export * from "./pg.js";
17
+ export * from "./pg-kysely.js";
17
18
  export * from "./r2-json.js";
18
19
  export * from "./rate-limit.js";
19
20
  export * from "./seo.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export * from "./mcp.js";
14
14
  export * from "./oauth.js";
15
15
  export * from "./ops-events.js";
16
16
  export * from "./pg.js";
17
+ export * from "./pg-kysely.js";
17
18
  export * from "./r2-json.js";
18
19
  export * from "./rate-limit.js";
19
20
  export * from "./seo.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { Kysely } from "kysely";
2
+ import postgres from "postgres";
3
+ export type PgConnectionSource = "hyperdrive" | "url" | "fallback";
4
+ export type PgSslMode = "auto" | "disable" | "require" | "verify-ca";
5
+ export type HyperdriveLike = {
6
+ connectionString?: string;
7
+ };
8
+ export type PgEnvLike = Record<string, unknown> & {
9
+ HYPERDRIVE?: HyperdriveLike;
10
+ PG_URL?: string;
11
+ PG_CA_CERT?: string;
12
+ };
13
+ export type ResolvedPgConnection = {
14
+ connectionString: string;
15
+ source: PgConnectionSource;
16
+ closeConnection: boolean;
17
+ caCert?: string;
18
+ };
19
+ export type PgConnectionOptions = {
20
+ hyperdriveKey?: string;
21
+ urlKey?: string;
22
+ caCertKey?: string;
23
+ fallbackConnectionString?: string;
24
+ requireConnection?: boolean;
25
+ closeConnection?: boolean;
26
+ };
27
+ export type PgClientOptions = {
28
+ max?: number;
29
+ prepare?: boolean;
30
+ idleTimeoutSeconds?: number;
31
+ connectTimeoutSeconds?: number;
32
+ sslMode?: PgSslMode;
33
+ };
34
+ export type KyselyPgOptions = PgConnectionOptions & PgClientOptions & {
35
+ onDestroyError?: (error: unknown) => void;
36
+ };
37
+ type PostgresOptions = NonNullable<Parameters<typeof postgres>[1]>;
38
+ export declare function resolvePgConnection(env: PgEnvLike, options?: PgConnectionOptions): ResolvedPgConnection;
39
+ export declare function postgresClientOptions(connection: Pick<ResolvedPgConnection, "connectionString" | "source" | "caCert">, options?: PgClientOptions): PostgresOptions;
40
+ export declare function createPostgresSql(connection: ResolvedPgConnection, options?: PgClientOptions): postgres.Sql;
41
+ export declare function createKyselyPg<Database>(env: PgEnvLike, options?: KyselyPgOptions): {
42
+ db: Kysely<Database>;
43
+ sql: postgres.Sql;
44
+ connection: ResolvedPgConnection;
45
+ destroy(): Promise<void>;
46
+ };
47
+ export declare function withKyselyPg<Database, Result>(env: PgEnvLike, fn: (db: Kysely<Database>, context: {
48
+ sql: postgres.Sql;
49
+ connection: ResolvedPgConnection;
50
+ }) => Promise<Result>, options?: KyselyPgOptions): Promise<Result>;
51
+ export declare function normalizePem(value?: string): string | undefined;
52
+ export {};
53
+ //# sourceMappingURL=pg-kysely.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-kysely.d.ts","sourceRoot":"","sources":["../src/pg-kysely.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,KAAK,GAAG,UAAU,CAAC;AACnE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAErE,MAAM,MAAM,cAAc,GAAG;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAChD,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAC/C,eAAe,GAAG;IAChB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,CAAC;AAEJ,KAAK,eAAe,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnE,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,SAAS,EACd,OAAO,GAAE,mBAAwB,GAChC,oBAAoB,CA2CtB;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,IAAI,CAAC,oBAAoB,EAAE,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAChF,OAAO,GAAE,eAAoB,GAC5B,eAAe,CAWjB;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE,eAAoB,GAC5B,QAAQ,CAAC,GAAG,CAEd;AAED,wBAAgB,cAAc,CAAC,QAAQ,EACrC,GAAG,EAAE,SAAS,EACd,OAAO,GAAE,eAAoB,GAC5B;IACD,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrB,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC;IAClB,UAAU,EAAE,oBAAoB,CAAC;IACjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B,CAoBA;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EACjD,GAAG,EAAE,SAAS,EACd,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC;IAAC,UAAU,EAAE,oBAAoB,CAAA;CAAE,KAAK,OAAO,CAAC,MAAM,CAAC,EAC/G,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI/D"}
@@ -0,0 +1,128 @@
1
+ import { Kysely } from "kysely";
2
+ import { PostgresJSDialect } from "kysely-postgres-js";
3
+ import postgres from "postgres";
4
+ export function resolvePgConnection(env, options = {}) {
5
+ const hyperdriveKey = options.hyperdriveKey ?? "HYPERDRIVE";
6
+ const urlKey = options.urlKey ?? "PG_URL";
7
+ const caCertKey = options.caCertKey ?? "PG_CA_CERT";
8
+ const hyperdrive = env[hyperdriveKey];
9
+ const hyperdriveString = cleanString(hyperdrive?.connectionString);
10
+ if (hyperdriveString) {
11
+ return {
12
+ connectionString: hyperdriveString,
13
+ source: "hyperdrive",
14
+ closeConnection: options.closeConnection ?? false,
15
+ };
16
+ }
17
+ const urlString = cleanString(env[urlKey]);
18
+ if (urlString) {
19
+ return {
20
+ connectionString: urlString,
21
+ source: "url",
22
+ closeConnection: options.closeConnection ?? true,
23
+ caCert: normalizePem(cleanString(env[caCertKey])),
24
+ };
25
+ }
26
+ const fallback = cleanString(options.fallbackConnectionString);
27
+ if (fallback) {
28
+ return {
29
+ connectionString: fallback,
30
+ source: "fallback",
31
+ closeConnection: options.closeConnection ?? true,
32
+ caCert: normalizePem(cleanString(env[caCertKey])),
33
+ };
34
+ }
35
+ if (options.requireConnection ?? true) {
36
+ throw new Error(`${urlKey} or ${hyperdriveKey}.connectionString is required for Postgres.`);
37
+ }
38
+ return {
39
+ connectionString: "",
40
+ source: "fallback",
41
+ closeConnection: options.closeConnection ?? true,
42
+ };
43
+ }
44
+ export function postgresClientOptions(connection, options = {}) {
45
+ const clientOptions = {
46
+ max: options.max ?? 1,
47
+ prepare: options.prepare ?? false,
48
+ idle_timeout: options.idleTimeoutSeconds ?? 20,
49
+ connect_timeout: options.connectTimeoutSeconds ?? 5,
50
+ };
51
+ const ssl = resolvePgSsl(connection, options.sslMode ?? "auto");
52
+ if (ssl !== undefined)
53
+ clientOptions.ssl = ssl;
54
+ return clientOptions;
55
+ }
56
+ export function createPostgresSql(connection, options = {}) {
57
+ return postgres(connection.connectionString, postgresClientOptions(connection, options));
58
+ }
59
+ export function createKyselyPg(env, options = {}) {
60
+ const connection = resolvePgConnection(env, options);
61
+ const sql = createPostgresSql(connection, options);
62
+ const db = new Kysely({
63
+ dialect: new PostgresJSDialect({ postgres: sql }),
64
+ });
65
+ return {
66
+ db,
67
+ sql,
68
+ connection,
69
+ async destroy() {
70
+ try {
71
+ await db.destroy();
72
+ }
73
+ catch (error) {
74
+ options.onDestroyError?.(error);
75
+ if (!options.onDestroyError)
76
+ throw error;
77
+ }
78
+ },
79
+ };
80
+ }
81
+ export async function withKyselyPg(env, fn, options = {}) {
82
+ const handle = createKyselyPg(env, options);
83
+ try {
84
+ return await fn(handle.db, { sql: handle.sql, connection: handle.connection });
85
+ }
86
+ finally {
87
+ if (handle.connection.closeConnection) {
88
+ await handle.destroy();
89
+ }
90
+ }
91
+ }
92
+ export function normalizePem(value) {
93
+ const trimmed = value?.trim();
94
+ if (!trimmed)
95
+ return undefined;
96
+ return trimmed.includes("\\n") ? trimmed.replace(/\\n/g, "\n") : trimmed;
97
+ }
98
+ function resolvePgSsl(connection, mode) {
99
+ if (mode === "disable")
100
+ return false;
101
+ if (mode === "require")
102
+ return "require";
103
+ if (mode === "verify-ca") {
104
+ if (!connection.caCert)
105
+ throw new Error("PG_CA_CERT is required when sslMode is verify-ca.");
106
+ return { ca: connection.caCert, rejectUnauthorized: true };
107
+ }
108
+ if (connection.source === "hyperdrive")
109
+ return false;
110
+ if (connection.caCert)
111
+ return { ca: connection.caCert, rejectUnauthorized: true };
112
+ if (isLocalPgUrl(connection.connectionString))
113
+ return false;
114
+ return "require";
115
+ }
116
+ function isLocalPgUrl(connectionString) {
117
+ try {
118
+ const host = new URL(connectionString).hostname;
119
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ function cleanString(value) {
126
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
127
+ }
128
+ //# sourceMappingURL=pg-kysely.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-kysely.js","sourceRoot":"","sources":["../src/pg-kysely.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ # Adgiro, Relin, and DirtSignal Commonality
2
+
3
+ This is the current direction for `@q32/core`: choose the best reusable pattern across the projects, then move the projects toward it.
4
+
5
+ ## Chosen Defaults
6
+
7
+ - Postgres access should use Kysely over raw SQL once a project has long-lived relational read models. DirtSignal has the largest raw `postgres` surface today, but Relin's Kysely layer is the better long-term default because it makes schema ownership, joins, transactions, and refactors safer.
8
+ - Cloudflare Workers should create PG clients per request, queue invocation, cron tick, or script scope. Hyperdrive owns pooling in Worker runtime; Node scripts and tests should close clients explicitly.
9
+ - PG connection resolution should prefer Hyperdrive, then `PG_URL`, then an explicit test fallback only when the caller opts in.
10
+ - Non-local PG should use TLS by default. A provided `PG_CA_CERT` should be normalized and used for certificate verification.
11
+ - PG migrations should be explicit, status/dry-run capable, and separated from runtime credentials. Relin's admin/runtime split is the better default; DirtSignal's typed migration-array model is better than ad hoc SQL file parsing for application-owned schema modules.
12
+ - D1 remains the default for small control-plane state, but shared auth, jobs, and ops-event tables should be configurable D1 modules, not app-local forks.
13
+
14
+ ## Core Modules To Grow
15
+
16
+ - `pg-kysely`: Kysely + `postgres.js` construction, Hyperdrive/PG_URL/CA-cert policy, and scoped `withKyselyPg`.
17
+ - `pg-migrations`: one migration runner that can consume SQL files or typed migration arrays, with status, dry-run, reset guard, and configurable migrations table.
18
+ - `d1-auth`: configurable users/orgs/memberships/identities/magic-link/session helpers. Apps keep policy and copy; core owns token hashing, one-time consume, identity upsert, and session persistence patterns.
19
+ - `mcp-oauth-d1`: the common OAuth client/code/token/device-flow repository used by Relin and DirtSignal, with app-provided principal lookup and entitlement policy.
20
+ - `d1-jobs`: Adgiro's richer job driver is closer to the target than the current minimal core job helper. Core should own enqueue policies, active locks, concurrency keys, delayed requeue, stale-running recovery, and linked ops events.
21
+ - `queue`: a typed Cloudflare Queue publisher/consumer adapter with inline mode for tests and local single-process execution.
22
+ - `ops-events`: converge on run IDs, event names, statuses/severity, target identifiers, metadata/error normalization, and best-effort writes.
23
+
24
+ ## Project Movement
25
+
26
+ - Relin should keep using Kysely for PG and replace its local connection lifecycle with `@q32/core/pg-kysely`.
27
+ - DirtSignal should migrate new PG repositories to Kysely first, then wrap existing raw `postgres` repositories behind Kysely-compatible interfaces as they change.
28
+ - Adgiro should adopt the shared D1 job driver shape and ops-event schema before adding PG. If it adds PG, it should start on Kysely rather than raw `postgres`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q32/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Shared TypeScript primitives for Q32 Cloudflare Worker projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -88,6 +88,10 @@
88
88
  "types": "./dist/pg.d.ts",
89
89
  "default": "./dist/pg.js"
90
90
  },
91
+ "./pg-kysely": {
92
+ "types": "./dist/pg-kysely.d.ts",
93
+ "default": "./dist/pg-kysely.js"
94
+ },
91
95
  "./r2-json": {
92
96
  "types": "./dist/r2-json.d.ts",
93
97
  "default": "./dist/r2-json.js"
@@ -115,6 +119,7 @@
115
119
  },
116
120
  "files": [
117
121
  "dist",
122
+ "src",
118
123
  "docs",
119
124
  "README.md",
120
125
  "LICENSE"
@@ -125,6 +130,11 @@
125
130
  "typescript": "^5.9.3",
126
131
  "vitest": "^4.0.15"
127
132
  },
133
+ "dependencies": {
134
+ "kysely": "^0.29.2",
135
+ "kysely-postgres-js": "^3.0.0",
136
+ "postgres": "^3.4.9"
137
+ },
128
138
  "publishConfig": {
129
139
  "access": "public"
130
140
  },
package/src/ai.ts ADDED
@@ -0,0 +1,42 @@
1
+ export type AiMessage = {
2
+ role: "system" | "user" | "assistant" | "tool";
3
+ content: string;
4
+ };
5
+
6
+ export type AiJsonRequest = {
7
+ model: string;
8
+ messages: AiMessage[];
9
+ responseName?: string;
10
+ temperature?: number;
11
+ };
12
+
13
+ export type AiJsonProvider = {
14
+ generateJson<T>(request: AiJsonRequest): Promise<T>;
15
+ };
16
+
17
+ export type AiUsage = {
18
+ inputTokens?: number;
19
+ outputTokens?: number;
20
+ totalTokens?: number;
21
+ };
22
+
23
+ export type AiResult<T> = {
24
+ value: T;
25
+ usage?: AiUsage;
26
+ providerMetadata?: Record<string, unknown>;
27
+ };
28
+
29
+ export function systemUserMessages(system: string, user: string): AiMessage[] {
30
+ return [
31
+ { role: "system", content: system },
32
+ { role: "user", content: user },
33
+ ];
34
+ }
35
+
36
+ export function extractJsonObject(text: string): unknown {
37
+ const trimmed = text.trim();
38
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) return JSON.parse(trimmed);
39
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
40
+ if (fenced) return JSON.parse(fenced[1].trim());
41
+ throw new Error("No JSON object found in model output.");
42
+ }
package/src/api.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { HttpError } from "./http.js";
2
+
3
+ export type ApiMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
4
+
5
+ export interface SchemaLike<T = unknown> {
6
+ parse(value: unknown): T;
7
+ }
8
+
9
+ export type ApiOperationSpec<TInput = unknown> = {
10
+ name: string;
11
+ title: string;
12
+ description: string;
13
+ method: ApiMethod;
14
+ path: string;
15
+ scope?: string;
16
+ inputSchema?: SchemaLike<TInput>;
17
+ openapi?: boolean;
18
+ };
19
+
20
+ export type ApiOperation<TContext, TInput = unknown, TOutput = unknown> = ApiOperationSpec<TInput> & {
21
+ handler: (context: TContext, input: TInput) => Promise<TOutput> | TOutput;
22
+ };
23
+
24
+ export type ApiOperationRegistry<TContext> = Record<string, ApiOperation<TContext, unknown, unknown>>;
25
+
26
+ export function defineApiOperation<TContext, TInput = unknown, TOutput = unknown>(
27
+ operation: ApiOperation<TContext, TInput, TOutput>,
28
+ ): ApiOperation<TContext, TInput, TOutput> {
29
+ return operation;
30
+ }
31
+
32
+ export function defineApiRegistry<TContext, TRegistry extends ApiOperationRegistry<TContext>>(
33
+ registry: TRegistry,
34
+ ): TRegistry {
35
+ const names = new Set<string>();
36
+ for (const [key, operation] of Object.entries(registry)) {
37
+ if (operation.name !== key) throw new Error(`API operation key/name mismatch: ${key} != ${operation.name}`);
38
+ if (names.has(operation.name)) throw new Error(`Duplicate API operation name: ${operation.name}`);
39
+ names.add(operation.name);
40
+ }
41
+ return registry;
42
+ }
43
+
44
+ export async function dispatchApiOperation<TContext>(
45
+ registry: ApiOperationRegistry<TContext>,
46
+ name: string,
47
+ context: TContext,
48
+ input: unknown,
49
+ ): Promise<unknown> {
50
+ const operation = registry[name];
51
+ if (!operation) throw new HttpError(404, `Unknown API operation: ${name}`, "unknown_operation");
52
+ const parsed = operation.inputSchema ? operation.inputSchema.parse(input) : input;
53
+ return operation.handler(context, parsed);
54
+ }
55
+
56
+ export function operationPathParameters(path: string): string[] {
57
+ const params = new Set<string>();
58
+ for (const match of path.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)) params.add(match[1]);
59
+ return [...params];
60
+ }
61
+
62
+ export function interpolateOperationPath(path: string, input: Record<string, unknown>): string {
63
+ return path.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key: string) => {
64
+ const value = input[key];
65
+ if (value === undefined || value === null || value === "") {
66
+ throw new HttpError(400, `Missing path parameter: ${key}`, "missing_path_parameter");
67
+ }
68
+ return encodeURIComponent(String(value));
69
+ });
70
+ }
71
+
72
+ export function openApiPathsForRegistry<TContext>(
73
+ registry: ApiOperationRegistry<TContext>,
74
+ ): Record<string, Record<string, Record<string, unknown>>> {
75
+ const paths: Record<string, Record<string, Record<string, unknown>>> = {};
76
+ for (const operation of Object.values(registry)) {
77
+ if (operation.openapi === false) continue;
78
+ const path = operation.path.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, "{$1}");
79
+ const method = operation.method.toLowerCase();
80
+ paths[path] ??= {};
81
+ paths[path][method] = {
82
+ operationId: operation.name,
83
+ summary: operation.title,
84
+ description: operation.description,
85
+ security: operation.scope ? [{ bearerAuth: [operation.scope] }] : undefined,
86
+ parameters: operationPathParameters(operation.path).map((name) => ({
87
+ name,
88
+ in: "path",
89
+ required: true,
90
+ schema: { type: "string" },
91
+ })),
92
+ responses: {
93
+ "200": {
94
+ description: "OK",
95
+ },
96
+ },
97
+ };
98
+ }
99
+ return paths;
100
+ }
package/src/billing.ts ADDED
@@ -0,0 +1,46 @@
1
+ export type BillingPlan = {
2
+ id: string;
3
+ name: string;
4
+ rank: number;
5
+ stripePriceId?: string;
6
+ limits?: Record<string, number>;
7
+ };
8
+
9
+ export type SubscriptionStatus =
10
+ | "trialing"
11
+ | "active"
12
+ | "past_due"
13
+ | "canceled"
14
+ | "unpaid"
15
+ | "incomplete"
16
+ | "incomplete_expired"
17
+ | "paused";
18
+
19
+ export type BillingCustomer = {
20
+ customerId: string;
21
+ email?: string;
22
+ planId?: string;
23
+ stripeCustomerId?: string;
24
+ stripeSubscriptionId?: string;
25
+ subscriptionStatus?: SubscriptionStatus;
26
+ };
27
+
28
+ export function planAtLeast(plans: BillingPlan[], actualPlanId: string | null | undefined, requiredPlanId: string): boolean {
29
+ const byId = new Map(plans.map((plan) => [plan.id, plan]));
30
+ const actual = actualPlanId ? byId.get(actualPlanId) : undefined;
31
+ const required = byId.get(requiredPlanId);
32
+ if (!actual || !required) return false;
33
+ return actual.rank >= required.rank;
34
+ }
35
+
36
+ export function activeSubscriptionStatuses(): SubscriptionStatus[] {
37
+ return ["trialing", "active"];
38
+ }
39
+
40
+ export function isActiveSubscriptionStatus(status: SubscriptionStatus | null | undefined): boolean {
41
+ return status === "trialing" || status === "active";
42
+ }
43
+
44
+ export function stripeEventAlreadyProcessedMessage(eventId: string): string {
45
+ return `Stripe event already processed: ${eventId}`;
46
+ }
@@ -0,0 +1,36 @@
1
+ export type WorkerQueueMessage<T = unknown> = {
2
+ jobId: string;
3
+ payload?: T;
4
+ };
5
+
6
+ export type DurableObjectIdLike = {
7
+ toString(): string;
8
+ };
9
+
10
+ export function requireD1(env: Record<string, unknown>, binding = "DB"): D1Database {
11
+ const db = env[binding];
12
+ if (!db || typeof db !== "object" || typeof (db as D1Database).prepare !== "function") {
13
+ throw new Error(`Missing D1 binding: ${binding}`);
14
+ }
15
+ return db as D1Database;
16
+ }
17
+
18
+ export function requireR2(env: Record<string, unknown>, binding: string): R2Bucket {
19
+ const bucket = env[binding];
20
+ if (!bucket || typeof bucket !== "object" || typeof (bucket as R2Bucket).put !== "function") {
21
+ throw new Error(`Missing R2 binding: ${binding}`);
22
+ }
23
+ return bucket as R2Bucket;
24
+ }
25
+
26
+ export function requireQueue<T = unknown>(env: Record<string, unknown>, binding: string): Queue<T> {
27
+ const queue = env[binding];
28
+ if (!queue || typeof queue !== "object" || typeof (queue as Queue<T>).send !== "function") {
29
+ throw new Error(`Missing Queue binding: ${binding}`);
30
+ }
31
+ return queue as Queue<T>;
32
+ }
33
+
34
+ export function isCloudflareScheduledEvent(value: unknown): value is ScheduledEvent {
35
+ return Boolean(value && typeof value === "object" && "scheduledTime" in value && "cron" in value);
36
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { fromBase64Url, randomBase64Url, toBase64Url } from "./ids.js";
2
+
3
+ const AES_GCM_IV_BYTES = 12;
4
+ const AES_GCM_KEY_BYTES = 32;
5
+
6
+ export type EncryptedJsonEnvelope = {
7
+ v: 1;
8
+ alg: "A256GCM";
9
+ iv: string;
10
+ ciphertext: string;
11
+ };
12
+
13
+ export function createEncryptionKey(): string {
14
+ return randomBase64Url(AES_GCM_KEY_BYTES);
15
+ }
16
+
17
+ export async function encryptJson(value: unknown, keyMaterial: string): Promise<EncryptedJsonEnvelope> {
18
+ const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
19
+ const key = await importAesKey(keyMaterial);
20
+ const plaintext = new TextEncoder().encode(JSON.stringify(value));
21
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
22
+ return {
23
+ v: 1,
24
+ alg: "A256GCM",
25
+ iv: toBase64Url(iv),
26
+ ciphertext: toBase64Url(new Uint8Array(ciphertext)),
27
+ };
28
+ }
29
+
30
+ export async function decryptJson<T>(envelope: EncryptedJsonEnvelope, keyMaterial: string): Promise<T> {
31
+ if (envelope.v !== 1 || envelope.alg !== "A256GCM") throw new Error("Unsupported encrypted JSON envelope.");
32
+ const key = await importAesKey(keyMaterial);
33
+ const plaintext = await crypto.subtle.decrypt(
34
+ { name: "AES-GCM", iv: toArrayBuffer(fromBase64Url(envelope.iv)) },
35
+ key,
36
+ toArrayBuffer(fromBase64Url(envelope.ciphertext)),
37
+ );
38
+ return JSON.parse(new TextDecoder().decode(plaintext)) as T;
39
+ }
40
+
41
+ async function importAesKey(keyMaterial: string): Promise<CryptoKey> {
42
+ let raw: Uint8Array;
43
+ try {
44
+ raw = fromBase64Url(keyMaterial);
45
+ } catch {
46
+ throw new Error("Encryption key must be 32 base64url-encoded bytes.");
47
+ }
48
+ if (raw.byteLength !== AES_GCM_KEY_BYTES) {
49
+ throw new Error("Encryption key must be 32 base64url-encoded bytes.");
50
+ }
51
+ return crypto.subtle.importKey("raw", toArrayBuffer(raw), "AES-GCM", false, ["encrypt", "decrypt"]);
52
+ }
53
+
54
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
55
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
56
+ }
package/src/d1.ts ADDED
@@ -0,0 +1,86 @@
1
+ export type D1Primitive = string | number | boolean | null | Uint8Array;
2
+
3
+ export interface D1StatementResult {
4
+ success: boolean;
5
+ meta: Record<string, unknown>;
6
+ results?: Record<string, unknown>[];
7
+ }
8
+
9
+ export interface D1PreparedStatementLike {
10
+ bind(...values: D1Primitive[]): D1PreparedStatementLike;
11
+ run(): Promise<D1StatementResult>;
12
+ first<T extends object = Record<string, unknown>>(): Promise<T | null>;
13
+ all<T extends object = Record<string, unknown>>(): Promise<{ results: T[] }>;
14
+ }
15
+
16
+ export interface D1DatabaseLike {
17
+ prepare(query: string): D1PreparedStatementLike;
18
+ batch(statements: D1PreparedStatementLike[]): Promise<D1StatementResult[]>;
19
+ exec(query: string): Promise<unknown>;
20
+ }
21
+
22
+ export type Migration = {
23
+ id: string;
24
+ sql: string;
25
+ };
26
+
27
+ export type MigrationResult = {
28
+ applied: string[];
29
+ skipped: string[];
30
+ };
31
+
32
+ export async function ensureMigrationsTable(db: D1DatabaseLike, tableName = "schema_migrations"): Promise<void> {
33
+ assertSafeIdentifier(tableName);
34
+ await db.exec(
35
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
36
+ id TEXT PRIMARY KEY,
37
+ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
38
+ )`,
39
+ );
40
+ }
41
+
42
+ export async function applyD1Migrations(
43
+ db: D1DatabaseLike,
44
+ migrations: Migration[],
45
+ options: { tableName?: string } = {},
46
+ ): Promise<MigrationResult> {
47
+ const tableName = options.tableName ?? "schema_migrations";
48
+ assertSafeIdentifier(tableName);
49
+ await ensureMigrationsTable(db, tableName);
50
+
51
+ const applied: string[] = [];
52
+ const skipped: string[] = [];
53
+
54
+ for (const migration of migrations) {
55
+ const existing = await db.prepare(`SELECT id FROM ${tableName} WHERE id = ? LIMIT 1`).bind(migration.id).first();
56
+ if (existing) {
57
+ skipped.push(migration.id);
58
+ continue;
59
+ }
60
+
61
+ await db.exec(migration.sql);
62
+ await db.prepare(`INSERT INTO ${tableName} (id) VALUES (?)`).bind(migration.id).run();
63
+ applied.push(migration.id);
64
+ }
65
+
66
+ return { applied, skipped };
67
+ }
68
+
69
+ export function parseJsonColumn<T>(value: string | null | undefined, fallback: T): T {
70
+ if (!value) return fallback;
71
+ try {
72
+ return JSON.parse(value) as T;
73
+ } catch {
74
+ return fallback;
75
+ }
76
+ }
77
+
78
+ export function stringifyJsonColumn(value: unknown): string {
79
+ return JSON.stringify(value ?? null);
80
+ }
81
+
82
+ function assertSafeIdentifier(value: string): void {
83
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
84
+ throw new Error(`Unsafe SQL identifier: ${value}`);
85
+ }
86
+ }