@isaijs/core 0.0.0-stage → 0.1.1
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 +19 -2
- package/dist/adapters/cache/cloudflare-kv/index.d.ts +12 -0
- package/dist/adapters/cache/cloudflare-kv/index.js +8 -0
- package/dist/adapters/database/d1/index.d.ts +18 -0
- package/dist/adapters/database/d1/index.js +33 -0
- package/dist/adapters/database/durable-object/index.d.ts +19 -0
- package/dist/adapters/database/durable-object/index.js +45 -0
- package/dist/adapters/database/sqlite-node/index.d.ts +8 -0
- package/dist/adapters/database/sqlite-node/index.js +53 -0
- package/dist/adapters/mail/gmail/index.d.ts +10 -0
- package/dist/adapters/mail/gmail/index.js +8 -0
- package/dist/adapters/payments/upi/index.d.ts +3 -0
- package/dist/adapters/payments/upi/index.js +8 -0
- package/dist/adapters/pdf/cloudflare-browser/index.d.ts +6 -0
- package/dist/adapters/pdf/cloudflare-browser/index.js +8 -0
- package/dist/adapters/queue/cloudflare/index.d.ts +10 -0
- package/dist/adapters/queue/cloudflare/index.js +8 -0
- package/dist/adapters/ratelimit/cloudflare/index.d.ts +12 -0
- package/dist/adapters/ratelimit/cloudflare/index.js +9 -0
- package/dist/adapters/ratelimit/memory/index.d.ts +12 -0
- package/dist/adapters/ratelimit/memory/index.js +27 -0
- package/dist/adapters/runtime/cloudflare/index.d.ts +33 -0
- package/dist/adapters/runtime/cloudflare/index.js +78 -0
- package/dist/adapters/storage/disk/index.d.ts +10 -0
- package/dist/adapters/storage/disk/index.js +66 -0
- package/dist/adapters/storage/memory/index.d.ts +8 -0
- package/dist/adapters/storage/memory/index.js +34 -0
- package/dist/adapters/storage/r2/index.d.ts +27 -0
- package/dist/adapters/storage/r2/index.js +35 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +3 -0
- package/dist/base64url-K_wCmvxR.js +1311 -0
- package/dist/case-BnpcvhdW.js +11 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1211 -0
- package/dist/client/index.d.ts +208 -0
- package/dist/client/index.js +214 -0
- package/dist/config-0PGFbJh6.js +446 -0
- package/dist/create-auth-y8E8cxkn.js +122 -0
- package/dist/data-layer-D9UPOK6L.js +292 -0
- package/dist/data-layer-ubPfZvFV.d.ts +982 -0
- package/dist/db/index.d.ts +4 -0
- package/dist/db/index.js +58 -0
- package/dist/errors-mCNLlXg9.js +13 -0
- package/dist/index-B6ipokoP.d.ts +631 -0
- package/dist/index-C-WmIKxT.d.ts +370 -0
- package/dist/index-Ce5p2-EB.d.ts +200 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3396 -0
- package/dist/issues-CKto95IW.d.ts +31 -0
- package/dist/make-WQpkYW8N.js +556 -0
- package/dist/migrator-6oWUefLA.js +3531 -0
- package/dist/policies-pFzslmSB.js +362 -0
- package/dist/ports/index.d.ts +2 -0
- package/dist/ports/index.js +0 -0
- package/dist/resource-C17NvNa3.js +730 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/seed-BObmdKdP.js +13 -0
- package/dist/storage-body-7dWyMszh.js +89 -0
- package/dist/syntax-CrUZFrS7.js +25 -0
- package/dist/tables-C4ipMFjg.js +178 -0
- package/dist/testing/index.d.ts +83 -0
- package/dist/testing/index.js +896 -0
- package/dist/types-DpOcIuDl.d.ts +1636 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation-FyOpSZqE.js +1299 -0
- package/package.json +131 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sedhupathi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# isai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Django-style, batteries-included, type-safe TypeScript backend framework. Cloudflare first, with no lock-in.
|
|
4
|
+
|
|
5
|
+
**Early and unstable (0.x):** the API changes between releases, and the docs aren't public yet.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add isai@npm:@isaijs/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Published as `@isaijs/core` and installed under the name `isai`, so imports read `isai/db` and the CLI is `isai`.
|
|
12
|
+
|
|
13
|
+
- Models with semantic field types, an ORM and migrations for SQLite (D1, Durable Objects, `node:sqlite`)
|
|
14
|
+
- Requests through one pipeline, resources (CRUD with filters, search, pagination), policies, auth with Google and invites
|
|
15
|
+
- Multi-tenancy, one database per tenant (a Durable Object on Cloudflare)
|
|
16
|
+
- Uploads, an audit log, a typed client, and the `isai` CLI (`dev`, `makemigrations`, `migrate`, `check`)
|
|
17
|
+
|
|
18
|
+
Apps import adapters by port: `isai/runtime/cloudflare`, `isai/database/d1`, `isai/storage/r2`.
|
|
19
|
+
|
|
20
|
+
MIT licensed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { r as Cache } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/cache/cloudflare-kv/index.d.ts
|
|
3
|
+
/** The parts of a KV binding this cache uses. */
|
|
4
|
+
export interface KvBinding {
|
|
5
|
+
get(key: string, type: "json"): Promise<unknown>;
|
|
6
|
+
put(key: string, value: string, options?: {
|
|
7
|
+
expirationTtl?: number;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
delete(key: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function cloudflareKv(_binding: KvBinding): Cache;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as notImplemented } from "../../../errors-mCNLlXg9.js";
|
|
2
|
+
//#region src/adapters/cache/cloudflare-kv/index.ts
|
|
3
|
+
/** `cloudflareKv`: a cache on Workers KV (eventually consistent). */
|
|
4
|
+
function cloudflareKv(_binding) {
|
|
5
|
+
return notImplemented("cloudflareKv()");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { cloudflareKv };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { a as DatabaseDriver, f as Row } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/database/d1/index.d.ts
|
|
3
|
+
/** The parts of a D1 prepared statement this driver uses. */
|
|
4
|
+
export interface D1Statement {
|
|
5
|
+
bind(...values: unknown[]): D1Statement;
|
|
6
|
+
all(): Promise<{
|
|
7
|
+
results: Row[];
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
/** The parts of a D1 binding this driver uses (structurally compatible with `wrangler types`). */
|
|
11
|
+
export interface D1Binding {
|
|
12
|
+
prepare(query: string): D1Statement;
|
|
13
|
+
batch(statements: D1Statement[]): Promise<{
|
|
14
|
+
results: Row[];
|
|
15
|
+
}[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare function d1(binding: D1Binding): DatabaseDriver;
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/adapters/database/d1/index.ts
|
|
2
|
+
/** D1 allows 100 bound parameters per statement; FTS5 is available. */
|
|
3
|
+
const capabilities = {
|
|
4
|
+
maxBoundParams: 100,
|
|
5
|
+
fullTextSearch: true
|
|
6
|
+
};
|
|
7
|
+
function d1(binding) {
|
|
8
|
+
const statement = (sql) => binding.prepare(sql.text).bind(...sql.params);
|
|
9
|
+
return {
|
|
10
|
+
kind: "database",
|
|
11
|
+
dialect: "sqlite",
|
|
12
|
+
capabilities,
|
|
13
|
+
query: async (sql) => rowsOf(await statement(sql).all()),
|
|
14
|
+
atomic: async (statements) => {
|
|
15
|
+
if (statements.length === 0) return [];
|
|
16
|
+
return (await binding.batch(statements.map(statement))).map(rowsOf);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function rowsOf(result) {
|
|
21
|
+
return result.results.map(normalizeRow);
|
|
22
|
+
}
|
|
23
|
+
/** D1 returns BLOBs as arrays of bytes; the port returns them as Uint8Array. */
|
|
24
|
+
function normalizeRow(row) {
|
|
25
|
+
for (const key in row) {
|
|
26
|
+
const value = row[key];
|
|
27
|
+
if (Array.isArray(value)) row[key] = Uint8Array.from(value);
|
|
28
|
+
else if (value instanceof ArrayBuffer) row[key] = new Uint8Array(value);
|
|
29
|
+
}
|
|
30
|
+
return row;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { d1 };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { a as DatabaseDriver, b as TenantDatabases, f as Row } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/database/durable-object/index.d.ts
|
|
3
|
+
/** The parts of `ctx.storage` this driver uses (structurally compatible with `wrangler types`). */
|
|
4
|
+
export interface DurableObjectStorageBinding {
|
|
5
|
+
readonly sql: {
|
|
6
|
+
exec(query: string, ...bindings: unknown[]): {
|
|
7
|
+
toArray(): Row[];
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
transactionSync<T>(fn: () => T): T;
|
|
11
|
+
}
|
|
12
|
+
/** The parts of a Durable Object namespace binding this adapter uses: tenants' objects, by name. */
|
|
13
|
+
export interface DurableObjectNamespaceBinding {
|
|
14
|
+
idFromName(name: string): unknown;
|
|
15
|
+
get(id: never): unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare function durableObject(storage: DurableObjectStorageBinding): DatabaseDriver;
|
|
18
|
+
export declare function durableObject(namespace: DurableObjectNamespaceBinding): TenantDatabases;
|
|
19
|
+
//#endregion
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/adapters/database/durable-object/index.ts
|
|
2
|
+
/** Durable Object SQLite allows 100 bound parameters per statement; FTS5 is available. */
|
|
3
|
+
const capabilities = {
|
|
4
|
+
maxBoundParams: 100,
|
|
5
|
+
fullTextSearch: true
|
|
6
|
+
};
|
|
7
|
+
function durableObject(binding) {
|
|
8
|
+
if (isStorage(binding)) return storageDriver(binding);
|
|
9
|
+
const namespace = binding;
|
|
10
|
+
/** A tenant's object, by the tenant's id. */
|
|
11
|
+
const stubOf = (id) => namespace.get(namespace.idFromName(id));
|
|
12
|
+
return {
|
|
13
|
+
kind: "tenantDatabases",
|
|
14
|
+
dialect: "sqlite",
|
|
15
|
+
forward: (id, request, tenant, requestId) => stubOf(id).handle(request, tenant, requestId),
|
|
16
|
+
call: (id, service, args, tenant) => stubOf(id).run(service, args, tenant),
|
|
17
|
+
open: (host) => {
|
|
18
|
+
if (!isStorage(host)) throw new TypeError("isai: a tenant's database opens from its Durable Object's storage (ctx.storage)");
|
|
19
|
+
return storageDriver(host);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isStorage(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && "sql" in value && "transactionSync" in value;
|
|
25
|
+
}
|
|
26
|
+
function storageDriver(storage) {
|
|
27
|
+
const run = (sql) => storage.sql.exec(sql.text, ...sql.params).toArray().map(normalizeRow);
|
|
28
|
+
return {
|
|
29
|
+
kind: "database",
|
|
30
|
+
dialect: "sqlite",
|
|
31
|
+
capabilities,
|
|
32
|
+
query: async (sql) => run(sql),
|
|
33
|
+
atomic: async (statements) => storage.transactionSync(() => statements.map(run))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Durable Objects return BLOBs as ArrayBuffer; the port returns them as Uint8Array. */
|
|
37
|
+
function normalizeRow(row) {
|
|
38
|
+
for (const key in row) {
|
|
39
|
+
const value = row[key];
|
|
40
|
+
if (value instanceof ArrayBuffer) row[key] = new Uint8Array(value);
|
|
41
|
+
}
|
|
42
|
+
return row;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { durableObject };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { a as DatabaseDriver } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/database/sqlite-node/index.d.ts
|
|
3
|
+
export interface SqliteNodeDriver extends DatabaseDriver {
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
/** A database file path, or `":memory:"` (the default) for a throwaway database. */
|
|
7
|
+
export declare function sqliteNode(path?: string): SqliteNodeDriver;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
//#region src/adapters/database/sqlite-node/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* `sqliteNode`: SQLite in a Node process, through the built-in `node:sqlite` (Node ≥ 22.13).
|
|
5
|
+
* Synchronous and in-process; `atomic` is `BEGIN IMMEDIATE … COMMIT`. Runs the core's
|
|
6
|
+
* tests, and self-hosting on Node later.
|
|
7
|
+
*/
|
|
8
|
+
const STATEMENT_CACHE_SIZE = 200;
|
|
9
|
+
/** A database file path, or `":memory:"` (the default) for a throwaway database. */
|
|
10
|
+
function sqliteNode(path = ":memory:") {
|
|
11
|
+
const db = new DatabaseSync(path);
|
|
12
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
13
|
+
if (path !== ":memory:") db.exec("PRAGMA journal_mode = WAL");
|
|
14
|
+
const capabilities = {
|
|
15
|
+
maxBoundParams: 32766,
|
|
16
|
+
fullTextSearch: db.prepare("SELECT sqlite_compileoption_used('ENABLE_FTS5') AS on_").get()?.on_ === 1
|
|
17
|
+
};
|
|
18
|
+
const statements = /* @__PURE__ */ new Map();
|
|
19
|
+
const prepare = (text) => {
|
|
20
|
+
let statement = statements.get(text);
|
|
21
|
+
if (statement) statements.delete(text);
|
|
22
|
+
else {
|
|
23
|
+
statement = db.prepare(text);
|
|
24
|
+
if (statements.size >= STATEMENT_CACHE_SIZE) {
|
|
25
|
+
const oldest = statements.keys().next().value;
|
|
26
|
+
if (oldest !== void 0) statements.delete(oldest);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
statements.set(text, statement);
|
|
30
|
+
return statement;
|
|
31
|
+
};
|
|
32
|
+
const run = (sql) => prepare(sql.text).all(...sql.params);
|
|
33
|
+
return {
|
|
34
|
+
kind: "database",
|
|
35
|
+
dialect: "sqlite",
|
|
36
|
+
capabilities,
|
|
37
|
+
query: async (sql) => run(sql),
|
|
38
|
+
atomic: async (batch) => {
|
|
39
|
+
db.exec("BEGIN IMMEDIATE");
|
|
40
|
+
try {
|
|
41
|
+
const results = batch.map(run);
|
|
42
|
+
db.exec("COMMIT");
|
|
43
|
+
return results;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
db.exec("ROLLBACK");
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
close: () => db.close()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { sqliteNode };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { s as MailDriver } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/mail/gmail/index.d.ts
|
|
3
|
+
export interface GmailOptions {
|
|
4
|
+
readonly clientId: string;
|
|
5
|
+
readonly clientSecret: string;
|
|
6
|
+
/** "workspace" refuses consumer @gmail.com accounts. */
|
|
7
|
+
readonly accounts?: "workspace" | "any";
|
|
8
|
+
}
|
|
9
|
+
export declare function gmail(_options: GmailOptions): MailDriver;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as notImplemented } from "../../../errors-mCNLlXg9.js";
|
|
2
|
+
//#region src/adapters/mail/gmail/index.ts
|
|
3
|
+
/** `gmail`: mail sent as a connected Google account (Gmail API, `gmail.send`). Needs only `fetch`. */
|
|
4
|
+
function gmail(_options) {
|
|
5
|
+
return notImplemented("gmail()");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { gmail };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as notImplemented } from "../../../errors-mCNLlXg9.js";
|
|
2
|
+
//#region src/adapters/payments/upi/index.ts
|
|
3
|
+
/** `upi`: UPI intent link / QR to a payee's UPI ID, verified manually by a scoped role. No gateway. */
|
|
4
|
+
function upi() {
|
|
5
|
+
return notImplemented("upi()");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { upi };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { l as PdfRenderer } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/pdf/cloudflare-browser/index.d.ts
|
|
3
|
+
/** A Browser Rendering binding (opaque here; used through @cloudflare/puppeteer). */
|
|
4
|
+
export type BrowserBinding = object;
|
|
5
|
+
export declare function cloudflareBrowser(_binding: BrowserBinding): PdfRenderer;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as notImplemented } from "../../../errors-mCNLlXg9.js";
|
|
2
|
+
//#region src/adapters/pdf/cloudflare-browser/index.ts
|
|
3
|
+
/** `cloudflareBrowser`: HTML → PDF with Cloudflare Browser Rendering. */
|
|
4
|
+
function cloudflareBrowser(_binding) {
|
|
5
|
+
return notImplemented("cloudflareBrowser()");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { cloudflareBrowser };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { u as Queue } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/queue/cloudflare/index.d.ts
|
|
3
|
+
/** The parts of a Queues producer binding this queue uses. */
|
|
4
|
+
export interface QueueBinding {
|
|
5
|
+
send(body: unknown, options?: {
|
|
6
|
+
delaySeconds?: number;
|
|
7
|
+
}): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export declare function cloudflareQueue(_binding: QueueBinding): Queue;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { n as notImplemented } from "../../../errors-mCNLlXg9.js";
|
|
2
|
+
//#region src/adapters/queue/cloudflare/index.ts
|
|
3
|
+
/** `cloudflareQueue`: tasks on Cloudflare Queues; consumed by the `cloudflare` runtime's queue handler. */
|
|
4
|
+
function cloudflareQueue(_binding) {
|
|
5
|
+
return notImplemented("cloudflareQueue()");
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { cloudflareQueue };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { d as RateLimiter } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/ratelimit/cloudflare/index.d.ts
|
|
3
|
+
/** The part of a Rate Limiting binding this uses. */
|
|
4
|
+
export interface RateLimitBinding {
|
|
5
|
+
limit(options: {
|
|
6
|
+
key: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
success: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare function cloudflareRateLimit(binding: RateLimitBinding): RateLimiter;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { d as RateLimiter } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/ratelimit/memory/index.d.ts
|
|
3
|
+
export interface MemoryRateLimitOptions {
|
|
4
|
+
/** Requests a key may make in a window. */
|
|
5
|
+
readonly limit: number;
|
|
6
|
+
/** The window's length, in seconds. */
|
|
7
|
+
readonly periodSeconds: number;
|
|
8
|
+
/** The time, for tests. */
|
|
9
|
+
readonly now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare function memoryRateLimit(options: MemoryRateLimitOptions): RateLimiter;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/adapters/ratelimit/memory/index.ts
|
|
2
|
+
/** Keys kept at most; the oldest go first (bots try many). */
|
|
3
|
+
const MAX_KEYS = 1e4;
|
|
4
|
+
function memoryRateLimit(options) {
|
|
5
|
+
const { limit, periodSeconds, now = Date.now } = options;
|
|
6
|
+
const windows = /* @__PURE__ */ new Map();
|
|
7
|
+
return {
|
|
8
|
+
kind: "rateLimiter",
|
|
9
|
+
allow: async (key) => {
|
|
10
|
+
const at = now();
|
|
11
|
+
const current = windows.get(key);
|
|
12
|
+
if (!current || at - current.start >= periodSeconds * 1e3) {
|
|
13
|
+
windows.delete(key);
|
|
14
|
+
if (windows.size >= MAX_KEYS) windows.delete(windows.keys().next().value);
|
|
15
|
+
windows.set(key, {
|
|
16
|
+
start: at,
|
|
17
|
+
count: 1
|
|
18
|
+
});
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
current.count += 1;
|
|
22
|
+
return current.count <= limit;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { memoryRateLimit };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { f as Row, m as ServiceRef, p as Runtime, v as TaskMessage } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
//#region src/adapters/runtime/cloudflare/index.d.ts
|
|
4
|
+
/** The parts of the Workers platform this runtime uses (structurally compatible with `wrangler types`). */
|
|
5
|
+
export interface QueueMessage<Body> {
|
|
6
|
+
readonly body: Body;
|
|
7
|
+
ack(): void;
|
|
8
|
+
retry(): void;
|
|
9
|
+
}
|
|
10
|
+
export interface MessageBatch<Body> {
|
|
11
|
+
readonly messages: readonly QueueMessage<Body>[];
|
|
12
|
+
}
|
|
13
|
+
export interface ScheduledController {
|
|
14
|
+
readonly scheduledTime: number;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkerHandlers<Env> {
|
|
17
|
+
fetch(request: Request, env: Env, ctx: unknown): Promise<Response>;
|
|
18
|
+
queue(batch: MessageBatch<TaskMessage>, env: Env, ctx: unknown): Promise<void>;
|
|
19
|
+
scheduled(controller: ScheduledController, env: Env, ctx: unknown): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** The Durable Object class a tenant lives in: what `app.ts` exports as `Tenant`. */
|
|
22
|
+
export type TenantClass<Env> = new (ctx: DurableObjectState, env: Env) => DurableObject<Env> & {
|
|
23
|
+
/** A request the front door forwarded to this tenant, with its `Tenant` row and request id. */
|
|
24
|
+
handle(request: Request, tenant: Row, requestId: string): Promise<Response>;
|
|
25
|
+
/** A service run in this tenant from outside a request (`app.tenant(id).run`). */
|
|
26
|
+
run(service: ServiceRef, args: readonly unknown[], tenant: Row): Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
/** Where `isai dev --seed` asks the local Worker to fill its databases. */
|
|
29
|
+
export declare const SEED_PATH = "/__isai/seed";
|
|
30
|
+
/** The header carrying the token `isai dev` gave `wrangler dev` (`--var ISAI_DEV_TOKEN:…`). */
|
|
31
|
+
export declare const DEV_TOKEN_HEADER = "X-Isai-Dev-Token";
|
|
32
|
+
export declare function cloudflare<Env>(): Runtime<Env, WorkerHandlers<Env>, TenantClass<Env>>;
|
|
33
|
+
//#endregion
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
//#region src/adapters/runtime/cloudflare/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* The `cloudflare` runtime: wires the core's neutral entry points to a Worker's handlers.
|
|
5
|
+
* fetch → app.handle(request)
|
|
6
|
+
* queue → app.runTask(message), acked or retried one message at a time
|
|
7
|
+
* scheduled → app.tick(now), the timer check (the one cron line in wrangler.jsonc)
|
|
8
|
+
*
|
|
9
|
+
* Workers hand the env to every invocation, so each handler starts the core with it;
|
|
10
|
+
* the core is built on the first call and reused after that.
|
|
11
|
+
*
|
|
12
|
+
* With isolated tenancy, `Tenant` is the Durable Object class each tenant lives in: it opens the
|
|
13
|
+
* tenant from its own storage, migrates it before any request (`blockConcurrencyWhile`), and
|
|
14
|
+
* handles what the front door forwards to it over RPC.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* One failing task never holds back the rest of its batch: each message is acked on
|
|
18
|
+
* success and retried on failure (Queues redelivers it, then dead-letters it).
|
|
19
|
+
*/
|
|
20
|
+
async function runBatch(core, batch) {
|
|
21
|
+
for (const message of batch.messages) try {
|
|
22
|
+
await core.runTask(message.body);
|
|
23
|
+
message.ack();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`isai: task ${message.body.task} (${message.body.id}) failed`, error);
|
|
26
|
+
message.retry();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Where `isai dev --seed` asks the local Worker to fill its databases. */
|
|
30
|
+
const SEED_PATH = "/__isai/seed";
|
|
31
|
+
/** The header carrying the token `isai dev` gave `wrangler dev` (`--var ISAI_DEV_TOKEN:…`). */
|
|
32
|
+
const DEV_TOKEN_HEADER = "X-Isai-Dev-Token";
|
|
33
|
+
/**
|
|
34
|
+
* `isai dev --seed <name>`: answered only when `wrangler dev` was started with `ISAI_DEV_TOKEN`
|
|
35
|
+
* (which `isai dev` sets, and a deployed Worker never has) and the request carries it. Otherwise
|
|
36
|
+
* the path is the app's like any other.
|
|
37
|
+
*/
|
|
38
|
+
async function devSeed(request, env, core) {
|
|
39
|
+
const token = env.ISAI_DEV_TOKEN;
|
|
40
|
+
if (typeof token !== "string" || token === "") return void 0;
|
|
41
|
+
const url = new URL(request.url);
|
|
42
|
+
if (url.pathname !== "/__isai/seed") return void 0;
|
|
43
|
+
if (request.method !== "POST" || request.headers.get("X-Isai-Dev-Token") !== token) return new Response(null, { status: 404 });
|
|
44
|
+
try {
|
|
45
|
+
return Response.json(await core.seed(url.searchParams.get("name") ?? ""));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function cloudflare() {
|
|
51
|
+
return {
|
|
52
|
+
name: "cloudflare",
|
|
53
|
+
serve: (start) => ({
|
|
54
|
+
fetch: async (request, env) => {
|
|
55
|
+
const core = start(env);
|
|
56
|
+
return await devSeed(request, env, core) ?? core.handle(request);
|
|
57
|
+
},
|
|
58
|
+
queue: (batch, env) => runBatch(start(env), batch),
|
|
59
|
+
scheduled: (controller, env) => start(env).tick(new Date(controller.scheduledTime))
|
|
60
|
+
}),
|
|
61
|
+
tenant: (start) => class Tenant extends DurableObject {
|
|
62
|
+
#tenant;
|
|
63
|
+
constructor(ctx, env) {
|
|
64
|
+
super(ctx, env);
|
|
65
|
+
this.#tenant = start(env).openTenant(ctx.storage);
|
|
66
|
+
ctx.blockConcurrencyWhile(() => this.#tenant.wake());
|
|
67
|
+
}
|
|
68
|
+
handle(request, tenant, requestId) {
|
|
69
|
+
return this.#tenant.handle(request, tenant, requestId);
|
|
70
|
+
}
|
|
71
|
+
run(service, args, tenant) {
|
|
72
|
+
return this.#tenant.run(service, args, tenant);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { DEV_TOKEN_HEADER, SEED_PATH, cloudflare };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { g as Storage } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/storage/disk/index.d.ts
|
|
3
|
+
export interface DiskStorageOptions {
|
|
4
|
+
/** The folder files are kept in, made when missing. */
|
|
5
|
+
readonly dir: string;
|
|
6
|
+
/** Where its files are served from, to make it a public storage. */
|
|
7
|
+
readonly publicUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function diskStorage(options: DiskStorageOptions): Storage;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { a as publicUrlOf, i as chunksOf } from "../../../storage-body-7dWyMszh.js";
|
|
2
|
+
import { dirname, resolve, sep } from "node:path";
|
|
3
|
+
import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
//#region src/adapters/storage/disk/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* `diskStorage`: files in a folder, for Node (development, or one server with its own disk). Each
|
|
8
|
+
* file's media type is kept beside it, in `<key>.type`. A file is written to a temporary name and
|
|
9
|
+
* renamed once whole, so a reader never sees half of one.
|
|
10
|
+
*/
|
|
11
|
+
function diskStorage(options) {
|
|
12
|
+
const root = resolve(options.dir);
|
|
13
|
+
const publicUrl = publicUrlOf(options.publicUrl);
|
|
14
|
+
/** The key's path, never outside the folder. */
|
|
15
|
+
const pathOf = (key) => {
|
|
16
|
+
const path = resolve(root, key);
|
|
17
|
+
if (key === "" || !path.startsWith(root + sep)) throw new TypeError(`isai: ${key} isn't a storage key`);
|
|
18
|
+
return path;
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
kind: "storage",
|
|
22
|
+
...publicUrl ? { publicUrl } : {},
|
|
23
|
+
put: async (key, body, { size, contentType }) => {
|
|
24
|
+
const path = pathOf(key);
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
const partial = `${path}.${crypto.randomUUID()}.part`;
|
|
27
|
+
const file = await open(partial, "w");
|
|
28
|
+
try {
|
|
29
|
+
for await (const chunk of chunksOf(body, size)) await file.write(chunk);
|
|
30
|
+
await file.close();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
await file.close().catch(() => {});
|
|
33
|
+
await rm(partial, { force: true });
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
await rename(partial, path);
|
|
37
|
+
await writeType(path, contentType);
|
|
38
|
+
},
|
|
39
|
+
get: async (key) => {
|
|
40
|
+
const path = pathOf(key);
|
|
41
|
+
const found = await stat(path).catch(() => void 0);
|
|
42
|
+
if (!found?.isFile()) return null;
|
|
43
|
+
const contentType = await readFile(`${path}.type`, "utf8").catch(() => void 0);
|
|
44
|
+
const handle = await open(path, "r");
|
|
45
|
+
return {
|
|
46
|
+
body: Readable.toWeb(handle.createReadStream()),
|
|
47
|
+
size: found.size,
|
|
48
|
+
...contentType ? { contentType } : {}
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
delete: async (key) => {
|
|
52
|
+
const path = pathOf(key);
|
|
53
|
+
await rm(path, { force: true });
|
|
54
|
+
await rm(`${path}.type`, { force: true });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function writeType(path, contentType) {
|
|
59
|
+
const partial = `${path}.type.${crypto.randomUUID()}.part`;
|
|
60
|
+
const file = await open(partial, "w");
|
|
61
|
+
await file.writeFile(contentType);
|
|
62
|
+
await file.close();
|
|
63
|
+
await rename(partial, `${path}.type`);
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { diskStorage };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { g as Storage } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/storage/memory/index.d.ts
|
|
3
|
+
export interface MemoryStorageOptions {
|
|
4
|
+
/** Where its files would be served from, to make it a public storage. */
|
|
5
|
+
readonly publicUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function memoryStorage(options?: MemoryStorageOptions): Storage;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { a as publicUrlOf, n as bytesOf, o as streamOf } from "../../../storage-body-7dWyMszh.js";
|
|
2
|
+
//#region src/adapters/storage/memory/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* `memoryStorage`: files in this process's memory, for tests (and a Node app's own tests). Nothing
|
|
5
|
+
* survives a restart; on Workers, use `r2`, and on Node, `diskStorage` or S3.
|
|
6
|
+
*/
|
|
7
|
+
function memoryStorage(options = {}) {
|
|
8
|
+
const files = /* @__PURE__ */ new Map();
|
|
9
|
+
const publicUrl = publicUrlOf(options.publicUrl);
|
|
10
|
+
return {
|
|
11
|
+
kind: "storage",
|
|
12
|
+
...publicUrl ? { publicUrl } : {},
|
|
13
|
+
put: async (key, body, { size, contentType }) => {
|
|
14
|
+
files.set(key, {
|
|
15
|
+
bytes: await bytesOf(body, size),
|
|
16
|
+
contentType
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
get: async (key) => {
|
|
20
|
+
const found = files.get(key);
|
|
21
|
+
if (!found) return null;
|
|
22
|
+
return {
|
|
23
|
+
body: streamOf(found.bytes.slice()),
|
|
24
|
+
size: found.bytes.byteLength,
|
|
25
|
+
contentType: found.contentType
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
delete: async (key) => {
|
|
29
|
+
files.delete(key);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { memoryStorage };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { g as Storage } from "../../../index-Ce5p2-EB.js";
|
|
2
|
+
//#region src/adapters/storage/r2/index.d.ts
|
|
3
|
+
/** The parts of an R2 binding this storage uses. */
|
|
4
|
+
export interface R2Binding {
|
|
5
|
+
put(key: string, value: ReadableStream | ArrayBuffer | Uint8Array, options?: R2PutOptions): Promise<unknown>;
|
|
6
|
+
get(key: string): Promise<R2ObjectBodyLike | null>;
|
|
7
|
+
delete(key: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
interface R2PutOptions {
|
|
10
|
+
readonly httpMetadata?: {
|
|
11
|
+
readonly contentType?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface R2ObjectBodyLike {
|
|
15
|
+
readonly body: ReadableStream<Uint8Array>;
|
|
16
|
+
readonly size: number;
|
|
17
|
+
readonly httpMetadata?: {
|
|
18
|
+
readonly contentType?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface R2Options {
|
|
22
|
+
/** Where the bucket serves its files (`https://media.example.com`), to make it a public storage. */
|
|
23
|
+
readonly publicUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function r2(binding: R2Binding, options?: R2Options): Storage;
|
|
26
|
+
//#endregion
|
|
27
|
+
export {};
|