@renoxium/sdk 0.1.0 → 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 -21
- package/README.md +178 -178
- package/package.json +85 -85
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Renoxium
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Renoxium
|
|
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,178 +1,178 @@
|
|
|
1
|
-
# `@renoxium/sdk`: the SDK for tenant code
|
|
2
|
-
|
|
3
|
-
Types and small helpers for Renox functions. Contract: [`docs/contracts/ops.md`](../docs/contracts/ops.md) version `1.1.0` and [`docs/contracts/ctx.d.ts`](../docs/contracts/ctx.d.ts). The types here are identical to the contract; `tests/contract.test-d.ts` fails the `check` script if they drift.
|
|
4
|
-
|
|
5
|
-
The package is published to npm as `@renoxium/sdk` (below, "Install"; npm refuses the unscoped name `renox` as too close to `redux` and `detox`, D28); inside this repository the workspace link (`"@renoxium/sdk": "workspace:*"`) resolves the same `exports`. It is runtime-neutral: no Bun, Node, or Deno APIs, so the same import works under `renox dev`, in the worker, and in `bun test`.
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
// functions/api/notes/[id].ts
|
|
9
|
-
import type { Handler } from "@renoxium/sdk";
|
|
10
|
-
|
|
11
|
-
export const GET: Handler = async ({ db, params }) => {
|
|
12
|
-
const note = await db.queryOne("select id, title from notes__notes where id = ?", [params.id as string]);
|
|
13
|
-
return note ? Response.json(note) : new Response(null, { status: 404 });
|
|
14
|
-
};
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
```sh
|
|
20
|
-
bun add @renoxium/sdk # or: npm install @renoxium/sdk
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`renox new` writes this dependency at the CLI's own version (`"@renoxium/sdk": "^<cli version>"`, `cli/src/commands/new.ts`): the CLI and the SDK are released together from one `cli-v<version>` tag, by `.github/workflows/release-cli.yml` (binaries) and `.github/workflows/release-sdk.yml` (this package, `npm publish --provenance`). Keep them at the same version.
|
|
24
|
-
|
|
25
|
-
The package is MIT licensed (`LICENSE`). It ships compiled JavaScript and declarations under `dist/` (`bun run build`, `tsconfig.build.json`), plus this README and the license; `exports` maps `@renoxium/sdk`, `@renoxium/sdk/kysely`, `@renoxium/sdk/drizzle`, `@renoxium/sdk/fakes` and `@renoxium/sdk/test` to them, so `tsc`, Node and editors resolve it like any published package. `@renoxium/sdk/test` is Bun-only (`bun:test`, `bun:sqlite`); the rest has no runtime dependency. `tests/pack.test.ts` packs the package, installs the tarball into a fresh `renox new` app outside the repository, type-checks it and runs a test with `@renoxium/sdk/fakes`. Every publish carries npm provenance from the release workflow (`npm view @renoxium/sdk --json | jq .dist.attestations`, or the Provenance badge on npmjs.com).
|
|
26
|
-
|
|
27
|
-
## F1 surface
|
|
28
|
-
|
|
29
|
-
| Export | Kind | Contract |
|
|
30
|
-
| --- | --- | --- |
|
|
31
|
-
| `Handler`, `Ctx`, `FunctionModule`, `HttpMethod`, `Params` | types | ops.md section 6 |
|
|
32
|
-
| `Db`, `DbStatements`, `ExecuteResult`, `Row`, `Value`, `Param` | types | ops.md section 1 |
|
|
33
|
-
| `Fetch` | type | ops.md section 2. The standard signature; the allowlist and blocked ranges are enforced in Rust |
|
|
34
|
-
| `Secrets` | type | ops.md section 3. Frozen; only declared and set secrets |
|
|
35
|
-
| `Log`, `LogLevel`, `LogFields`, `JsonValue` | types | ops.md section 4 |
|
|
36
|
-
| `Deadline` | type | ops.md section 5 |
|
|
37
|
-
| `RenoxError` and its subclasses (`DbPermissionError`, `DbBusyError`, `DbTimeoutError`, `DbQuotaError`, `DbConstraintError`, `DbSqlError`, `DbUsageError`, `FetchBlockedError`, `FetchError`, `FetchEgressLimitError`, `DeadlineError`) | classes | ops.md section 13 |
|
|
38
|
-
| `RenoxErrorCode`, `ResponseErrorCode`, `RENOX_ERROR_CODES`, `RESPONSE_ERROR_CODES`, `ErrorResponseBody` | types and constants | ops.md section 13, errors.md |
|
|
39
|
-
| `isRenoxError(e)` | function | Checks `e.code` instead of class identity: errors raised by the runtime glue are not guaranteed to be instances of the class a tenant bundled |
|
|
40
|
-
| `defineHandlers({ GET, POST })` | function | Optional sugar; returns its argument unchanged |
|
|
41
|
-
| `LoaderContext`, `ActionContext`, `Session`, `Loader`, `Action`, `ActionResult`, `PageData`, `PageModule` | types | ops.md section 14 (pages) |
|
|
42
|
-
| `definePage({ load, actions })` | function | Optional sugar for a page module; returns its argument unchanged |
|
|
43
|
-
| `validate(input, Schema)`, `ValidationError`, `formToObject`, `StandardSchemaV1` | function, class, types | Input validation (`docs/04-isolation-security.md`), below |
|
|
44
|
-
|
|
45
|
-
`ctx` fields in F1: `req`, `url`, `params`, `headers`, `db`, `fetch`, `secrets`, `log`, `deadline`. Pages add `fragment` and `session` (loaders and actions) and `action`, `form` (actions); an action's `db` is its own transaction (`DbStatements`, no `transaction()`).
|
|
46
|
-
|
|
47
|
-
## Pages
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
// pages/products/[id].ts
|
|
51
|
-
import { type Action, type Loader, validate } from "@renoxium/sdk";
|
|
52
|
-
import { z } from "zod";
|
|
53
|
-
|
|
54
|
-
export const load: Loader = async ({ db, params }) => {
|
|
55
|
-
const product = await db.queryOne("select id, name, stock from app__products where id = ?", [params.id as string]);
|
|
56
|
-
return product ? { product } : new Response(null, { status: 404 });
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const AddToCart = z.object({ qty: z.coerce.number().int().min(1).max(99) });
|
|
60
|
-
|
|
61
|
-
export const actions: Record<string, Action> = {
|
|
62
|
-
addToCart: async ({ form, db, session, params }) => {
|
|
63
|
-
const { qty } = await validate(form, AddToCart);
|
|
64
|
-
await db.execute("insert into app__cart (session_id, product_id, qty) values (?, ?, ?)", [session.id, params.id as string, qty]);
|
|
65
|
-
return { fragments: ["cart_badge"] };
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## `validate(input, Schema)`
|
|
71
|
-
|
|
72
|
-
Validates before the handler body and returns the schema's output (transforms and defaults applied, typed from the schema). The schema is any [Standard Schema v1](https://standardschema.dev) implementation: zod 3.24+, valibot 1.0+, arktype. The SDK has no dependency on any of them.
|
|
73
|
-
|
|
74
|
-
| `input` | Read as |
|
|
75
|
-
| --- | --- |
|
|
76
|
-
| `Request` | Its body, once: JSON for `application/json` and `*+json`, a form for `application/x-www-form-urlencoded` and `multipart/form-data`. Any other content type fails |
|
|
77
|
-
| `FormData` (`ctx.form`), `URLSearchParams` (`ctx.url.searchParams`) | An object: a key seen once maps to its value (`string`, or `File`), a repeated key to an array of its values (`formToObject`) |
|
|
78
|
-
| anything else | As is |
|
|
79
|
-
|
|
80
|
-
On failure it throws `ValidationError` with `issues: { message, path }[]` (malformed JSON and an unsupported content type are one issue on the root path `[]`). It is not a `RenoxError` (no platform code): uncaught, it is a 500 like any other exception, so catch it and answer with `e.toResponse()` (400, `{ "issues": [...] }`) or your own page. A schema that is not a Standard Schema is a `TypeError`.
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
export const POST: Handler = async ({ req }) => {
|
|
84
|
-
try {
|
|
85
|
-
const order = await validate(req, OrderSchema);
|
|
86
|
-
return Response.json(await place(order), { status: 201 });
|
|
87
|
-
} catch (e) {
|
|
88
|
-
if (e instanceof ValidationError) return e.toResponse();
|
|
89
|
-
throw e;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
The op-level types (`Ops`, `FetchRequest`, `FetchResponseHead`, `TxHandle`) are not exported: tenant code never sees ops, and the glue that calls them lives in `crates/renox-runtime/js`.
|
|
95
|
-
|
|
96
|
-
## Modules (`ops.md` 1.3.0, #226, #236)
|
|
97
|
-
|
|
98
|
-
`Ctx` carries `module`, `config`, `services`, `events`, `hooks`, and `password` (types identical to `docs/contracts/ctx.d.ts`). They are untyped there; the CLI generates the precise types of the installed modules from `renox.lock` and the manifests:
|
|
99
|
-
|
|
100
|
-
- `.renox/types/modules.d.ts`: `Modules` (per module: `services`, `events` with payload types from the manifest's JSON schemas, `hooks` with argument types, `config`) and `ModuleCtx<M>`, the ctx of module `M` (`"app"` by default).
|
|
101
|
-
- `.renox/types/renox-modules.d.ts`: `declare module "renox:modules"` with one export per module.
|
|
102
|
-
|
|
103
|
-
Service signatures are the module's own `services.ts`, inferred by the type checker (`ServicesOf<typeof import(".../services.ts")>`): the leading `ctx` is dropped and the result becomes a promise, so a call with a wrong argument is a type error. Helpers that only add types: `defineServices({...})`, `defineHooks({ hooks, on })`, `on<Payload>(handler)`. Errors: `ModuleUndeclaredError`, `ModuleServiceError` (`module`, `service`, `details.cause_code`), `EventPayloadError`.
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { defineServices } from "@renoxium/sdk";
|
|
107
|
-
import type { ModuleCtx } from "../../.renox/types/modules";
|
|
108
|
-
|
|
109
|
-
export const { chargeUser } = defineServices({
|
|
110
|
-
async chargeUser(ctx: ModuleCtx<"billing">, userId: string, amount: number) {
|
|
111
|
-
const user = await ctx.services.auth.getUser(userId); // typed from auth's services.ts
|
|
112
|
-
return ctx.events.emit("billing.invoice_paid", { invoice_id: "inv_1", user_id: userId, amount }); // payload checked
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Query builders: `@renoxium/sdk/kysely`, `@renoxium/sdk/drizzle` (#237)
|
|
118
|
-
|
|
119
|
-
Thin drivers over `ctx.db` (`docs/06-data.md`, "API layers"): every statement goes to `db.query` or `db.execute` with its `?` parameters in order, so the limits and the module authorizer apply as to raw SQL; there is no second execution path. Both libraries are optional peer dependencies (`kysely` 0.29, `drizzle-orm` 0.45): an app that does not import the subpath does not need them.
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import { kysely } from "@renoxium/sdk/kysely";
|
|
123
|
-
import type { Tables } from "../.renox/types/db";
|
|
124
|
-
const rows = await kysely<Tables>(ctx).selectFrom("app__products").selectAll().where("id", "=", id).execute();
|
|
125
|
-
await kysely<Tables>(ctx).transaction().execute(async (tx) => { /* one IMMEDIATE transaction, a savepoint when nested */ });
|
|
126
|
-
|
|
127
|
-
import { drizzle } from "@renoxium/sdk/drizzle";
|
|
128
|
-
await drizzle(ctx).select().from(products).where(eq(products.id, id));
|
|
129
|
-
await ctx.db.transaction(async (tx) => drizzle({ db: tx }).insert(orders).values(row));
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Drizzle's own `db.transaction()` sends `BEGIN` as SQL and is refused; use `ctx.db.transaction` with `drizzle({ db: tx })`. Drizzle wraps a failed statement in `DrizzleQueryError`; the platform's `RenoxError` is its `cause`. Kysely's error is the `RenoxError` itself. No streaming. Tests: `tests/{kysely,drizzle}.test.ts` (unit over bun:sqlite; on the worker with `RENOX_WORKER_PATH`, including the refusal of a table outside the app's prefix with the same error as raw SQL).
|
|
133
|
-
|
|
134
|
-
## Realtime, tools, jobs, TOTP and WebAuthn (#284)
|
|
135
|
-
|
|
136
|
-
| Module | Exports | What for |
|
|
137
|
-
| --- | --- | --- |
|
|
138
|
-
| `src/realtime.ts` | `defineWs`, `defineSse`, `topic(...segments)`, `isTopic` | Type a `ws` / `sse` export; build a topic the platform accepts (`ops.md` section 18). `ctx.realtime` itself is typed in `ctx.ts` |
|
|
139
|
-
| `src/tools.ts` | `tool(def)`, `toolInputSchema(schema)`, `INPUT_KEYWORDS` | Declare an App MCP tool (`ops.md` section 19). `tool()` checks the name, description, action and Standard Schema input, and defaults `confirm` to false. `toolInputSchema` gives the JSON Schema `renox build` records, through `~standard.jsonSchema.input` (zod 4.2+) or `toJSONSchema()`, reduced to the platform's keywords |
|
|
140
|
-
| `src/jobs.ts` | `defineJob`, `parseSchedule`, `isTimeZone` | A job's `schedule` (five cron fields) and `tz` (`ops.md` section 20); `renox build` uses the same parser (V27) |
|
|
141
|
-
| `src/totp.ts` | `generateTotpSecret`, `totpUri`, `totp`, `verifyTotp`, `base32Encode`, `base32Decode` | RFC 6238 on Web Crypto (SHA-1, SHA-256, SHA-512). `verifyTotp` accepts ±1 step (`window`) and refuses a step not after `lastStep`, so a code works once. Tests: the RFC's vectors |
|
|
142
|
-
| `src/webauthn.ts` | `verifyRegistration`, `verifyAssertion`, `webauthnChallenge`, `WebAuthnError`, `decodeCbor`, `base64url`, `fromBase64url` | Passkeys on Web Crypto. Registration checks the client data (type, challenge, origin), the RP id hash, user presence (and verification when asked), the attested credential (COSE ES256 or RS256), and `none` or `packed` self attestation; certificate chains are refused. Assertion checks the same, verifies the signature over `authenticatorData` and the client data hash, and refuses a counter that did not grow. Errors carry a `reason` |
|
|
143
|
-
|
|
144
|
-
Comparisons of secrets go through `timingSafeEqual` (`crypto.ts`).
|
|
145
|
-
|
|
146
|
-
## Authorization (`ops.md` 1.5.0 section 17, #283)
|
|
147
|
-
|
|
148
|
-
`ctx.authz` (`require`, `can`, `canBatch`, `explain`, `roles`), `ctx.principal`, the `Guard` export type, `ctx.db.scoped(scope)` and the errors `ForbiddenError` (`RENOX_FORBIDDEN`) and `DbScopeError` (`RENOX_DB_SCOPE`) are typed in `ctx.ts` and `errors.ts`, identical to `docs/contracts/ctx.d.ts`. `src/authz.ts` adds:
|
|
149
|
-
|
|
150
|
-
| Export | What it does |
|
|
151
|
-
| --- | --- |
|
|
152
|
-
| `defineGuard({ action, resource? })` | Types a page, function, ws or sse module's `export const guard` and returns it unchanged. It checks the shape: `action` is `<resource>.<verb>`, and `resource.id` is a literal or `":<param>"`. `renox build` reads the literal into the route entry |
|
|
153
|
-
| `resource(type, row)` | A `ResourceRef` from a database row (`id` as a string, the row's columns as attributes) |
|
|
154
|
-
| `isForbidden(e)` | Whether `e` is the `RENOX_FORBIDDEN` of a deny |
|
|
155
|
-
| `userScope(principal, extra?)` | `{ user_id, ...extra }` for `ctx.db.scoped()`; throws `ForbiddenError` without a user |
|
|
156
|
-
| `ScopedDb`, `ActionName` | The type of `ctx.db.scoped(...)`, and the `<resource>.<verb>` string type |
|
|
157
|
-
|
|
158
|
-
`@renoxium/sdk/test`: `policyCases(dir?)` and `policies()` run the policy test cases of `policies/tests/*.json` as tests (cli README, "Authorization").
|
|
159
|
-
|
|
160
|
-
## Reserved, not yet available
|
|
161
|
-
|
|
162
|
-
Names reserved by ops.md section 8. They are not in `Ctx` yet, so using one is a type error until its phase ships.
|
|
163
|
-
|
|
164
|
-
| Phase | `ctx` fields and exports |
|
|
165
|
-
| --- | --- |
|
|
166
|
-
| F2 | `kv`; streamed `fetch` request bodies; the page export `guard`; `cors` and `frame` options |
|
|
167
|
-
| F4 | query builders (`@renoxium/sdk/kysely`, `@renoxium/sdk/drizzle`, #237); `auth` and session data come from the `auth` module as services, not ctx fields |
|
|
168
|
-
| F5 | `ws` and `sse` exports' typed helpers (#284); `authz`, `realtime` and `db.scoped()` are typed above |
|
|
169
|
-
| F6 | `schedule` export for jobs; `datasource(name)` (F6–F7) |
|
|
170
|
-
| F7 | `ai`, `domains` |
|
|
171
|
-
| not scheduled | `offload` |
|
|
172
|
-
|
|
173
|
-
## Development
|
|
174
|
-
|
|
175
|
-
```sh
|
|
176
|
-
bun run --filter renox check # tsc on src/ and the contract test, then on the bun tests
|
|
177
|
-
bun test sdk # error-code table, class behaviour, validate() with zod and valibot
|
|
178
|
-
```
|
|
1
|
+
# `@renoxium/sdk`: the SDK for tenant code
|
|
2
|
+
|
|
3
|
+
Types and small helpers for Renox functions. Contract: [`docs/contracts/ops.md`](../docs/contracts/ops.md) version `1.1.0` and [`docs/contracts/ctx.d.ts`](../docs/contracts/ctx.d.ts). The types here are identical to the contract; `tests/contract.test-d.ts` fails the `check` script if they drift.
|
|
4
|
+
|
|
5
|
+
The package is published to npm as `@renoxium/sdk` (below, "Install"; npm refuses the unscoped name `renox` as too close to `redux` and `detox`, D28); inside this repository the workspace link (`"@renoxium/sdk": "workspace:*"`) resolves the same `exports`. It is runtime-neutral: no Bun, Node, or Deno APIs, so the same import works under `renox dev`, in the worker, and in `bun test`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// functions/api/notes/[id].ts
|
|
9
|
+
import type { Handler } from "@renoxium/sdk";
|
|
10
|
+
|
|
11
|
+
export const GET: Handler = async ({ db, params }) => {
|
|
12
|
+
const note = await db.queryOne("select id, title from notes__notes where id = ?", [params.id as string]);
|
|
13
|
+
return note ? Response.json(note) : new Response(null, { status: 404 });
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun add @renoxium/sdk # or: npm install @renoxium/sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`renox new` writes this dependency at the CLI's own version (`"@renoxium/sdk": "^<cli version>"`, `cli/src/commands/new.ts`): the CLI and the SDK are released together from one `cli-v<version>` tag, by `.github/workflows/release-cli.yml` (binaries) and `.github/workflows/release-sdk.yml` (this package, `npm publish --provenance`). Keep them at the same version.
|
|
24
|
+
|
|
25
|
+
The package is MIT licensed (`LICENSE`). It ships compiled JavaScript and declarations under `dist/` (`bun run build`, `tsconfig.build.json`), plus this README and the license; `exports` maps `@renoxium/sdk`, `@renoxium/sdk/kysely`, `@renoxium/sdk/drizzle`, `@renoxium/sdk/fakes` and `@renoxium/sdk/test` to them, so `tsc`, Node and editors resolve it like any published package. `@renoxium/sdk/test` is Bun-only (`bun:test`, `bun:sqlite`); the rest has no runtime dependency. `tests/pack.test.ts` packs the package, installs the tarball into a fresh `renox new` app outside the repository, type-checks it and runs a test with `@renoxium/sdk/fakes`. Every publish carries npm provenance from the release workflow (`npm view @renoxium/sdk --json | jq .dist.attestations`, or the Provenance badge on npmjs.com).
|
|
26
|
+
|
|
27
|
+
## F1 surface
|
|
28
|
+
|
|
29
|
+
| Export | Kind | Contract |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `Handler`, `Ctx`, `FunctionModule`, `HttpMethod`, `Params` | types | ops.md section 6 |
|
|
32
|
+
| `Db`, `DbStatements`, `ExecuteResult`, `Row`, `Value`, `Param` | types | ops.md section 1 |
|
|
33
|
+
| `Fetch` | type | ops.md section 2. The standard signature; the allowlist and blocked ranges are enforced in Rust |
|
|
34
|
+
| `Secrets` | type | ops.md section 3. Frozen; only declared and set secrets |
|
|
35
|
+
| `Log`, `LogLevel`, `LogFields`, `JsonValue` | types | ops.md section 4 |
|
|
36
|
+
| `Deadline` | type | ops.md section 5 |
|
|
37
|
+
| `RenoxError` and its subclasses (`DbPermissionError`, `DbBusyError`, `DbTimeoutError`, `DbQuotaError`, `DbConstraintError`, `DbSqlError`, `DbUsageError`, `FetchBlockedError`, `FetchError`, `FetchEgressLimitError`, `DeadlineError`) | classes | ops.md section 13 |
|
|
38
|
+
| `RenoxErrorCode`, `ResponseErrorCode`, `RENOX_ERROR_CODES`, `RESPONSE_ERROR_CODES`, `ErrorResponseBody` | types and constants | ops.md section 13, errors.md |
|
|
39
|
+
| `isRenoxError(e)` | function | Checks `e.code` instead of class identity: errors raised by the runtime glue are not guaranteed to be instances of the class a tenant bundled |
|
|
40
|
+
| `defineHandlers({ GET, POST })` | function | Optional sugar; returns its argument unchanged |
|
|
41
|
+
| `LoaderContext`, `ActionContext`, `Session`, `Loader`, `Action`, `ActionResult`, `PageData`, `PageModule` | types | ops.md section 14 (pages) |
|
|
42
|
+
| `definePage({ load, actions })` | function | Optional sugar for a page module; returns its argument unchanged |
|
|
43
|
+
| `validate(input, Schema)`, `ValidationError`, `formToObject`, `StandardSchemaV1` | function, class, types | Input validation (`docs/04-isolation-security.md`), below |
|
|
44
|
+
|
|
45
|
+
`ctx` fields in F1: `req`, `url`, `params`, `headers`, `db`, `fetch`, `secrets`, `log`, `deadline`. Pages add `fragment` and `session` (loaders and actions) and `action`, `form` (actions); an action's `db` is its own transaction (`DbStatements`, no `transaction()`).
|
|
46
|
+
|
|
47
|
+
## Pages
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// pages/products/[id].ts
|
|
51
|
+
import { type Action, type Loader, validate } from "@renoxium/sdk";
|
|
52
|
+
import { z } from "zod";
|
|
53
|
+
|
|
54
|
+
export const load: Loader = async ({ db, params }) => {
|
|
55
|
+
const product = await db.queryOne("select id, name, stock from app__products where id = ?", [params.id as string]);
|
|
56
|
+
return product ? { product } : new Response(null, { status: 404 });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const AddToCart = z.object({ qty: z.coerce.number().int().min(1).max(99) });
|
|
60
|
+
|
|
61
|
+
export const actions: Record<string, Action> = {
|
|
62
|
+
addToCart: async ({ form, db, session, params }) => {
|
|
63
|
+
const { qty } = await validate(form, AddToCart);
|
|
64
|
+
await db.execute("insert into app__cart (session_id, product_id, qty) values (?, ?, ?)", [session.id, params.id as string, qty]);
|
|
65
|
+
return { fragments: ["cart_badge"] };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## `validate(input, Schema)`
|
|
71
|
+
|
|
72
|
+
Validates before the handler body and returns the schema's output (transforms and defaults applied, typed from the schema). The schema is any [Standard Schema v1](https://standardschema.dev) implementation: zod 3.24+, valibot 1.0+, arktype. The SDK has no dependency on any of them.
|
|
73
|
+
|
|
74
|
+
| `input` | Read as |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `Request` | Its body, once: JSON for `application/json` and `*+json`, a form for `application/x-www-form-urlencoded` and `multipart/form-data`. Any other content type fails |
|
|
77
|
+
| `FormData` (`ctx.form`), `URLSearchParams` (`ctx.url.searchParams`) | An object: a key seen once maps to its value (`string`, or `File`), a repeated key to an array of its values (`formToObject`) |
|
|
78
|
+
| anything else | As is |
|
|
79
|
+
|
|
80
|
+
On failure it throws `ValidationError` with `issues: { message, path }[]` (malformed JSON and an unsupported content type are one issue on the root path `[]`). It is not a `RenoxError` (no platform code): uncaught, it is a 500 like any other exception, so catch it and answer with `e.toResponse()` (400, `{ "issues": [...] }`) or your own page. A schema that is not a Standard Schema is a `TypeError`.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
export const POST: Handler = async ({ req }) => {
|
|
84
|
+
try {
|
|
85
|
+
const order = await validate(req, OrderSchema);
|
|
86
|
+
return Response.json(await place(order), { status: 201 });
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (e instanceof ValidationError) return e.toResponse();
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The op-level types (`Ops`, `FetchRequest`, `FetchResponseHead`, `TxHandle`) are not exported: tenant code never sees ops, and the glue that calls them lives in `crates/renox-runtime/js`.
|
|
95
|
+
|
|
96
|
+
## Modules (`ops.md` 1.3.0, #226, #236)
|
|
97
|
+
|
|
98
|
+
`Ctx` carries `module`, `config`, `services`, `events`, `hooks`, and `password` (types identical to `docs/contracts/ctx.d.ts`). They are untyped there; the CLI generates the precise types of the installed modules from `renox.lock` and the manifests:
|
|
99
|
+
|
|
100
|
+
- `.renox/types/modules.d.ts`: `Modules` (per module: `services`, `events` with payload types from the manifest's JSON schemas, `hooks` with argument types, `config`) and `ModuleCtx<M>`, the ctx of module `M` (`"app"` by default).
|
|
101
|
+
- `.renox/types/renox-modules.d.ts`: `declare module "renox:modules"` with one export per module.
|
|
102
|
+
|
|
103
|
+
Service signatures are the module's own `services.ts`, inferred by the type checker (`ServicesOf<typeof import(".../services.ts")>`): the leading `ctx` is dropped and the result becomes a promise, so a call with a wrong argument is a type error. Helpers that only add types: `defineServices({...})`, `defineHooks({ hooks, on })`, `on<Payload>(handler)`. Errors: `ModuleUndeclaredError`, `ModuleServiceError` (`module`, `service`, `details.cause_code`), `EventPayloadError`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { defineServices } from "@renoxium/sdk";
|
|
107
|
+
import type { ModuleCtx } from "../../.renox/types/modules";
|
|
108
|
+
|
|
109
|
+
export const { chargeUser } = defineServices({
|
|
110
|
+
async chargeUser(ctx: ModuleCtx<"billing">, userId: string, amount: number) {
|
|
111
|
+
const user = await ctx.services.auth.getUser(userId); // typed from auth's services.ts
|
|
112
|
+
return ctx.events.emit("billing.invoice_paid", { invoice_id: "inv_1", user_id: userId, amount }); // payload checked
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Query builders: `@renoxium/sdk/kysely`, `@renoxium/sdk/drizzle` (#237)
|
|
118
|
+
|
|
119
|
+
Thin drivers over `ctx.db` (`docs/06-data.md`, "API layers"): every statement goes to `db.query` or `db.execute` with its `?` parameters in order, so the limits and the module authorizer apply as to raw SQL; there is no second execution path. Both libraries are optional peer dependencies (`kysely` 0.29, `drizzle-orm` 0.45): an app that does not import the subpath does not need them.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { kysely } from "@renoxium/sdk/kysely";
|
|
123
|
+
import type { Tables } from "../.renox/types/db";
|
|
124
|
+
const rows = await kysely<Tables>(ctx).selectFrom("app__products").selectAll().where("id", "=", id).execute();
|
|
125
|
+
await kysely<Tables>(ctx).transaction().execute(async (tx) => { /* one IMMEDIATE transaction, a savepoint when nested */ });
|
|
126
|
+
|
|
127
|
+
import { drizzle } from "@renoxium/sdk/drizzle";
|
|
128
|
+
await drizzle(ctx).select().from(products).where(eq(products.id, id));
|
|
129
|
+
await ctx.db.transaction(async (tx) => drizzle({ db: tx }).insert(orders).values(row));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Drizzle's own `db.transaction()` sends `BEGIN` as SQL and is refused; use `ctx.db.transaction` with `drizzle({ db: tx })`. Drizzle wraps a failed statement in `DrizzleQueryError`; the platform's `RenoxError` is its `cause`. Kysely's error is the `RenoxError` itself. No streaming. Tests: `tests/{kysely,drizzle}.test.ts` (unit over bun:sqlite; on the worker with `RENOX_WORKER_PATH`, including the refusal of a table outside the app's prefix with the same error as raw SQL).
|
|
133
|
+
|
|
134
|
+
## Realtime, tools, jobs, TOTP and WebAuthn (#284)
|
|
135
|
+
|
|
136
|
+
| Module | Exports | What for |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `src/realtime.ts` | `defineWs`, `defineSse`, `topic(...segments)`, `isTopic` | Type a `ws` / `sse` export; build a topic the platform accepts (`ops.md` section 18). `ctx.realtime` itself is typed in `ctx.ts` |
|
|
139
|
+
| `src/tools.ts` | `tool(def)`, `toolInputSchema(schema)`, `INPUT_KEYWORDS` | Declare an App MCP tool (`ops.md` section 19). `tool()` checks the name, description, action and Standard Schema input, and defaults `confirm` to false. `toolInputSchema` gives the JSON Schema `renox build` records, through `~standard.jsonSchema.input` (zod 4.2+) or `toJSONSchema()`, reduced to the platform's keywords |
|
|
140
|
+
| `src/jobs.ts` | `defineJob`, `parseSchedule`, `isTimeZone` | A job's `schedule` (five cron fields) and `tz` (`ops.md` section 20); `renox build` uses the same parser (V27) |
|
|
141
|
+
| `src/totp.ts` | `generateTotpSecret`, `totpUri`, `totp`, `verifyTotp`, `base32Encode`, `base32Decode` | RFC 6238 on Web Crypto (SHA-1, SHA-256, SHA-512). `verifyTotp` accepts ±1 step (`window`) and refuses a step not after `lastStep`, so a code works once. Tests: the RFC's vectors |
|
|
142
|
+
| `src/webauthn.ts` | `verifyRegistration`, `verifyAssertion`, `webauthnChallenge`, `WebAuthnError`, `decodeCbor`, `base64url`, `fromBase64url` | Passkeys on Web Crypto. Registration checks the client data (type, challenge, origin), the RP id hash, user presence (and verification when asked), the attested credential (COSE ES256 or RS256), and `none` or `packed` self attestation; certificate chains are refused. Assertion checks the same, verifies the signature over `authenticatorData` and the client data hash, and refuses a counter that did not grow. Errors carry a `reason` |
|
|
143
|
+
|
|
144
|
+
Comparisons of secrets go through `timingSafeEqual` (`crypto.ts`).
|
|
145
|
+
|
|
146
|
+
## Authorization (`ops.md` 1.5.0 section 17, #283)
|
|
147
|
+
|
|
148
|
+
`ctx.authz` (`require`, `can`, `canBatch`, `explain`, `roles`), `ctx.principal`, the `Guard` export type, `ctx.db.scoped(scope)` and the errors `ForbiddenError` (`RENOX_FORBIDDEN`) and `DbScopeError` (`RENOX_DB_SCOPE`) are typed in `ctx.ts` and `errors.ts`, identical to `docs/contracts/ctx.d.ts`. `src/authz.ts` adds:
|
|
149
|
+
|
|
150
|
+
| Export | What it does |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `defineGuard({ action, resource? })` | Types a page, function, ws or sse module's `export const guard` and returns it unchanged. It checks the shape: `action` is `<resource>.<verb>`, and `resource.id` is a literal or `":<param>"`. `renox build` reads the literal into the route entry |
|
|
153
|
+
| `resource(type, row)` | A `ResourceRef` from a database row (`id` as a string, the row's columns as attributes) |
|
|
154
|
+
| `isForbidden(e)` | Whether `e` is the `RENOX_FORBIDDEN` of a deny |
|
|
155
|
+
| `userScope(principal, extra?)` | `{ user_id, ...extra }` for `ctx.db.scoped()`; throws `ForbiddenError` without a user |
|
|
156
|
+
| `ScopedDb`, `ActionName` | The type of `ctx.db.scoped(...)`, and the `<resource>.<verb>` string type |
|
|
157
|
+
|
|
158
|
+
`@renoxium/sdk/test`: `policyCases(dir?)` and `policies()` run the policy test cases of `policies/tests/*.json` as tests (cli README, "Authorization").
|
|
159
|
+
|
|
160
|
+
## Reserved, not yet available
|
|
161
|
+
|
|
162
|
+
Names reserved by ops.md section 8. They are not in `Ctx` yet, so using one is a type error until its phase ships.
|
|
163
|
+
|
|
164
|
+
| Phase | `ctx` fields and exports |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| F2 | `kv`; streamed `fetch` request bodies; the page export `guard`; `cors` and `frame` options |
|
|
167
|
+
| F4 | query builders (`@renoxium/sdk/kysely`, `@renoxium/sdk/drizzle`, #237); `auth` and session data come from the `auth` module as services, not ctx fields |
|
|
168
|
+
| F5 | `ws` and `sse` exports' typed helpers (#284); `authz`, `realtime` and `db.scoped()` are typed above |
|
|
169
|
+
| F6 | `schedule` export for jobs; `datasource(name)` (F6–F7) |
|
|
170
|
+
| F7 | `ai`, `domains` |
|
|
171
|
+
| not scheduled | `offload` |
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
bun run --filter renox check # tsc on src/ and the contract test, then on the bun tests
|
|
177
|
+
bun test sdk # error-code table, class behaviour, validate() with zod and valibot
|
|
178
|
+
```
|
package/package.json
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@renoxium/sdk",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Types and helpers for Renox tenant code: ctx (db, fetch, secrets, log, deadline), handlers, pages, modules, errors, test fakes",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"homepage": "https://github.com/arif-rachim/renox.rs/tree/main/sdk",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/arif-rachim/renox.rs.git",
|
|
10
|
-
"directory": "sdk"
|
|
11
|
-
},
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/arif-rachim/renox.rs/issues"
|
|
14
|
-
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"renox",
|
|
17
|
-
"renoxium",
|
|
18
|
-
"sdk",
|
|
19
|
-
"hosting",
|
|
20
|
-
"multi-tenant",
|
|
21
|
-
"sqlite",
|
|
22
|
-
"typescript",
|
|
23
|
-
"htmx"
|
|
24
|
-
],
|
|
25
|
-
"type": "module",
|
|
26
|
-
"exports": {
|
|
27
|
-
".": {
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"default": "./dist/index.js"
|
|
30
|
-
},
|
|
31
|
-
"./kysely": {
|
|
32
|
-
"types": "./dist/kysely.d.ts",
|
|
33
|
-
"default": "./dist/kysely.js"
|
|
34
|
-
},
|
|
35
|
-
"./drizzle": {
|
|
36
|
-
"types": "./dist/drizzle.d.ts",
|
|
37
|
-
"default": "./dist/drizzle.js"
|
|
38
|
-
},
|
|
39
|
-
"./fakes": {
|
|
40
|
-
"types": "./dist/fakes.d.ts",
|
|
41
|
-
"default": "./dist/fakes.js"
|
|
42
|
-
},
|
|
43
|
-
"./test": {
|
|
44
|
-
"types": "./dist/test.d.ts",
|
|
45
|
-
"default": "./dist/test.js"
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
"files": [
|
|
49
|
-
"dist",
|
|
50
|
-
"README.md",
|
|
51
|
-
"LICENSE"
|
|
52
|
-
],
|
|
53
|
-
"engines": {
|
|
54
|
-
"bun": ">=1.4",
|
|
55
|
-
"node": ">=20"
|
|
56
|
-
},
|
|
57
|
-
"publishConfig": {
|
|
58
|
-
"access": "public",
|
|
59
|
-
"provenance": true
|
|
60
|
-
},
|
|
61
|
-
"scripts": {
|
|
62
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
63
|
-
"prepare": "node scripts/prepare.mjs",
|
|
64
|
-
"check": "tsc --noEmit -p . && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p client",
|
|
65
|
-
"test": "bun test"
|
|
66
|
-
},
|
|
67
|
-
"devDependencies": {
|
|
68
|
-
"valibot": "1.5.0",
|
|
69
|
-
"zod": "4.6.7",
|
|
70
|
-
"drizzle-orm": "0.45.4",
|
|
71
|
-
"kysely": "0.29.6"
|
|
72
|
-
},
|
|
73
|
-
"peerDependencies": {
|
|
74
|
-
"drizzle-orm": ">=0.45.0 <1.0.0",
|
|
75
|
-
"kysely": ">=0.29.0 <0.30.0"
|
|
76
|
-
},
|
|
77
|
-
"peerDependenciesMeta": {
|
|
78
|
-
"drizzle-orm": {
|
|
79
|
-
"optional": true
|
|
80
|
-
},
|
|
81
|
-
"kysely": {
|
|
82
|
-
"optional": true
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@renoxium/sdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Types and helpers for Renox tenant code: ctx (db, fetch, secrets, log, deadline), handlers, pages, modules, errors, test fakes",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/arif-rachim/renox.rs/tree/main/sdk",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/arif-rachim/renox.rs.git",
|
|
10
|
+
"directory": "sdk"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/arif-rachim/renox.rs/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"renox",
|
|
17
|
+
"renoxium",
|
|
18
|
+
"sdk",
|
|
19
|
+
"hosting",
|
|
20
|
+
"multi-tenant",
|
|
21
|
+
"sqlite",
|
|
22
|
+
"typescript",
|
|
23
|
+
"htmx"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./kysely": {
|
|
32
|
+
"types": "./dist/kysely.d.ts",
|
|
33
|
+
"default": "./dist/kysely.js"
|
|
34
|
+
},
|
|
35
|
+
"./drizzle": {
|
|
36
|
+
"types": "./dist/drizzle.d.ts",
|
|
37
|
+
"default": "./dist/drizzle.js"
|
|
38
|
+
},
|
|
39
|
+
"./fakes": {
|
|
40
|
+
"types": "./dist/fakes.d.ts",
|
|
41
|
+
"default": "./dist/fakes.js"
|
|
42
|
+
},
|
|
43
|
+
"./test": {
|
|
44
|
+
"types": "./dist/test.d.ts",
|
|
45
|
+
"default": "./dist/test.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"bun": ">=1.4",
|
|
55
|
+
"node": ">=20"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"provenance": true
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
63
|
+
"prepare": "node scripts/prepare.mjs",
|
|
64
|
+
"check": "tsc --noEmit -p . && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p client",
|
|
65
|
+
"test": "bun test"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"valibot": "1.5.0",
|
|
69
|
+
"zod": "4.6.7",
|
|
70
|
+
"drizzle-orm": "0.45.4",
|
|
71
|
+
"kysely": "0.29.6"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"drizzle-orm": ">=0.45.0 <1.0.0",
|
|
75
|
+
"kysely": ">=0.29.0 <0.30.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"drizzle-orm": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"kysely": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|