@renoxium/sdk 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 +177 -2
- package/dist/authz.d.ts +31 -0
- package/dist/authz.js +39 -0
- package/dist/crypto.d.ts +6 -0
- package/dist/crypto.js +26 -0
- package/dist/ctx.d.ts +486 -0
- package/dist/ctx.js +13 -0
- package/dist/db.d.ts +36 -0
- package/dist/db.js +3 -0
- package/dist/drizzle.d.ts +8 -0
- package/dist/drizzle.js +42 -0
- package/dist/errors.d.ts +261 -0
- package/dist/errors.js +232 -0
- package/dist/fakes.d.ts +35 -0
- package/dist/fakes.js +144 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -0
- package/dist/jobs.d.ts +16 -0
- package/dist/jobs.js +72 -0
- package/dist/kysely.d.ts +11 -0
- package/dist/kysely.js +126 -0
- package/dist/modules.d.ts +72 -0
- package/dist/modules.js +30 -0
- package/dist/pages.d.ts +53 -0
- package/dist/pages.js +14 -0
- package/dist/realtime.d.ts +15 -0
- package/dist/realtime.js +40 -0
- package/dist/redirect.d.ts +19 -0
- package/dist/redirect.js +57 -0
- package/dist/test.d.ts +169 -0
- package/dist/test.js +381 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +105 -0
- package/dist/totp.d.ts +40 -0
- package/dist/totp.js +111 -0
- package/dist/validate.d.ts +76 -0
- package/dist/validate.js +105 -0
- package/dist/webauthn.d.ts +63 -0
- package/dist/webauthn.js +283 -0
- package/package.json +83 -4
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,178 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@renoxium/sdk`: the SDK for tenant code
|
|
2
2
|
|
|
3
|
-
|
|
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/dist/authz.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Guard, ResourceRef } from "./ctx.ts";
|
|
2
|
+
import type { Db, Scope } from "./db.ts";
|
|
3
|
+
import { ForbiddenError } from "./errors.ts";
|
|
4
|
+
/** An action name `<resource>.<verb>` (manifest.md `[authz].actions`). */
|
|
5
|
+
export type ActionName = `${string}.${string}`;
|
|
6
|
+
/** What `ctx.db.scoped(scope)` returns: a `Db` whose owned tables carry the scope's owner predicate. */
|
|
7
|
+
export type ScopedDb = Db;
|
|
8
|
+
/**
|
|
9
|
+
* Types the `guard` export of a page, function, ws or sse module and returns
|
|
10
|
+
* it unchanged. `renox build` reads it into the route entry; the cell
|
|
11
|
+
* evaluates it before the loader or handler (and on fragment requests).
|
|
12
|
+
* `resource.id` is a literal or `":<param>"`, a parameter of the route.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export const guard = defineGuard({ action: "invoice.read", resource: { type: "Invoice", id: ":id" } });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function defineGuard<const G extends Guard & {
|
|
19
|
+
action: ActionName;
|
|
20
|
+
}>(guard: G): G;
|
|
21
|
+
/** A resource reference for `authz.require` / `can`: a row with its entity type. */
|
|
22
|
+
export declare function resource<T extends Readonly<Record<string, unknown>> & {
|
|
23
|
+
readonly id: string | number;
|
|
24
|
+
}>(type: string, row: T): ResourceRef & Omit<T, "id" | "type">;
|
|
25
|
+
/** Whether `e` is the `RENOX_FORBIDDEN` of a deny (`authz.require`, `realtime.subscribe`). */
|
|
26
|
+
export declare function isForbidden(e: unknown): e is ForbiddenError;
|
|
27
|
+
/** The scope of the request's own user, for `ctx.db.scoped()`; throws when there is no user. */
|
|
28
|
+
export declare function userScope(principal: {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly kind: string;
|
|
31
|
+
} | null, extra?: Omit<Scope, "user_id">): Scope;
|
package/dist/authz.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Authorization helpers (#283; docs/contracts/ops.md section 17,
|
|
2
|
+
// docs/11-authorization.md). The `Authz` surface itself is on `ctx.authz`
|
|
3
|
+
// (ctx.ts); these are the typed pieces a module writes around it.
|
|
4
|
+
import { ForbiddenError, isRenoxError } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Types the `guard` export of a page, function, ws or sse module and returns
|
|
7
|
+
* it unchanged. `renox build` reads it into the route entry; the cell
|
|
8
|
+
* evaluates it before the loader or handler (and on fragment requests).
|
|
9
|
+
* `resource.id` is a literal or `":<param>"`, a parameter of the route.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const guard = defineGuard({ action: "invoice.read", resource: { type: "Invoice", id: ":id" } });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function defineGuard(guard) {
|
|
16
|
+
if (typeof guard?.action !== "string" || !/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/u.test(guard.action)) {
|
|
17
|
+
throw new TypeError(`defineGuard: action must be <resource>.<verb>, got ${JSON.stringify(guard?.action)}`);
|
|
18
|
+
}
|
|
19
|
+
const r = guard.resource;
|
|
20
|
+
if (r !== undefined && (typeof r.type !== "string" || typeof r.id !== "string" || r.id.length === 0)) {
|
|
21
|
+
throw new TypeError("defineGuard: resource is { type, id } with id a literal or \":<param>\"");
|
|
22
|
+
}
|
|
23
|
+
return guard;
|
|
24
|
+
}
|
|
25
|
+
/** A resource reference for `authz.require` / `can`: a row with its entity type. */
|
|
26
|
+
export function resource(type, row) {
|
|
27
|
+
return { ...row, type, id: String(row.id) };
|
|
28
|
+
}
|
|
29
|
+
/** Whether `e` is the `RENOX_FORBIDDEN` of a deny (`authz.require`, `realtime.subscribe`). */
|
|
30
|
+
export function isForbidden(e) {
|
|
31
|
+
return e instanceof ForbiddenError || (isRenoxError(e) && e.code === "RENOX_FORBIDDEN");
|
|
32
|
+
}
|
|
33
|
+
/** The scope of the request's own user, for `ctx.db.scoped()`; throws when there is no user. */
|
|
34
|
+
export function userScope(principal, extra = {}) {
|
|
35
|
+
if (principal === null || principal.kind !== "user") {
|
|
36
|
+
throw new ForbiddenError("A signed-in user is required.", { action: "db.scoped", resource: null, reason: "deny", policy: null });
|
|
37
|
+
}
|
|
38
|
+
return { ...extra, user_id: principal.id };
|
|
39
|
+
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `true` when `a` and `b` hold the same bytes (strings compare as UTF-8).
|
|
3
|
+
* The time taken depends only on the longer input's length, never on where
|
|
4
|
+
* the inputs differ; inputs of different lengths are not equal.
|
|
5
|
+
*/
|
|
6
|
+
export declare function timingSafeEqual(a: string | ArrayBuffer | ArrayBufferView, b: string | ArrayBuffer | ArrayBufferView): boolean;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Constant-time comparison (docs/24-threat-model.md, "Timing attacks on
|
|
2
|
+
// signature or token comparison"; #111). Use it for every comparison of a
|
|
3
|
+
// secret value: signatures (`webhook.verify` uses it when built), tokens,
|
|
4
|
+
// API keys. `===` on strings returns at the first differing character.
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
function bytes(v) {
|
|
7
|
+
if (typeof v === "string")
|
|
8
|
+
return encoder.encode(v);
|
|
9
|
+
if (v instanceof ArrayBuffer)
|
|
10
|
+
return new Uint8Array(v);
|
|
11
|
+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `true` when `a` and `b` hold the same bytes (strings compare as UTF-8).
|
|
15
|
+
* The time taken depends only on the longer input's length, never on where
|
|
16
|
+
* the inputs differ; inputs of different lengths are not equal.
|
|
17
|
+
*/
|
|
18
|
+
export function timingSafeEqual(a, b) {
|
|
19
|
+
const x = bytes(a);
|
|
20
|
+
const y = bytes(b);
|
|
21
|
+
const n = Math.max(x.length, y.length);
|
|
22
|
+
let diff = x.length ^ y.length;
|
|
23
|
+
for (let i = 0; i < n; i++)
|
|
24
|
+
diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
|
|
25
|
+
return diff === 0;
|
|
26
|
+
}
|