@renoxium/sdk 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,261 @@
1
+ import type { JobState } from "./ctx.ts";
2
+ /** Codes an op rejects with inside tenant code ("thrown" and "both" in ops.md section 13). */
3
+ export declare const RENOX_ERROR_CODES: readonly ["RENOX_DB_AUTH", "RENOX_DB_BUSY", "RENOX_DB_TIMEOUT", "RENOX_DB_QUOTA", "RENOX_DB_CONSTRAINT", "RENOX_DB_SQL", "RENOX_DB_PARAM", "RENOX_DB_COLUMN_NAME", "RENOX_DB_MULTI_STATEMENT", "RENOX_DB_RESULT_LIMIT", "RENOX_DB_TX_STATE", "RENOX_DB_SCOPE", "RENOX_FETCH_BLOCKED", "RENOX_FETCH_FAILED", "RENOX_FETCH_EGRESS_LIMIT", "RENOX_DEADLINE", "RENOX_NO_REQUEST", "RENOX_SECRETS_CONSUMED", "RENOX_MODULE_UNDECLARED", "RENOX_MODULE_SERVICE", "RENOX_EVENT_PAYLOAD", "RENOX_FORBIDDEN", "RENOX_REALTIME_LIMIT", "RENOX_REALTIME_BACKPRESSURE", "RENOX_JOB_NOT_FOUND", "RENOX_JOB_STATE", "RENOX_STORAGE_QUOTA", "RENOX_STORAGE_TYPE", "RENOX_STORAGE_TOO_LARGE", "RENOX_STORAGE_KEY", "RENOX_STORAGE_NOT_FOUND", "RENOX_AI_QUOTA", "RENOX_AI_UNAVAILABLE", "RENOX_PDF_LIMIT"];
4
+ /** Codes the platform answers with when a request ends ("response" and "both"). */
5
+ export declare const RESPONSE_ERROR_CODES: readonly ["RENOX_DEADLINE", "RENOX_CPU_LIMIT", "RENOX_OOM", "RENOX_BUSY", "RENOX_INTERNAL", "RENOX_BODY_TOO_LARGE", "RENOX_CSRF", "RENOX_RENDER_LIMIT", "RENOX_MODULE_EVAL", "RENOX_FORBIDDEN", "RENOX_REALTIME_LIMIT", "RENOX_REALTIME_BACKPRESSURE", "RENOX_TOOL_INPUT", "RENOX_TOOL_SCOPE", "RENOX_TOOL_CONFIRM", "RENOX_IMAGE_RATE", "RENOX_WEBHOOK_SIGNATURE"];
6
+ export type DbErrorCode = "RENOX_DB_AUTH" | "RENOX_DB_BUSY" | "RENOX_DB_TIMEOUT" | "RENOX_DB_QUOTA" | "RENOX_DB_CONSTRAINT" | "RENOX_DB_SQL" | "RENOX_DB_PARAM" | "RENOX_DB_COLUMN_NAME" | "RENOX_DB_MULTI_STATEMENT" | "RENOX_DB_RESULT_LIMIT" | "RENOX_DB_TX_STATE" | "RENOX_DB_SCOPE";
7
+ export type FetchErrorCode = "RENOX_FETCH_BLOCKED" | "RENOX_FETCH_FAILED" | "RENOX_FETCH_EGRESS_LIMIT";
8
+ /** Codes of the module broker (ops.md section 15). */
9
+ export type ModuleErrorCode = "RENOX_MODULE_UNDECLARED" | "RENOX_MODULE_SERVICE" | "RENOX_EVENT_PAYLOAD";
10
+ /** A policy denied (ops.md section 17): thrown by `authz.require` and `realtime.subscribe`, answered by a `guard`. */
11
+ export type AuthzErrorCode = "RENOX_FORBIDDEN";
12
+ /** The realtime limits (ops.md section 18): thrown by the realtime ops, and the reason of a platform close. */
13
+ export type RealtimeErrorCode = "RENOX_REALTIME_LIMIT" | "RENOX_REALTIME_BACKPRESSURE";
14
+ /** `/mcp` refusals before a tool runs (ops.md section 19), as JSON-RPC errors. */
15
+ export type ToolErrorCode = "RENOX_TOOL_INPUT" | "RENOX_TOOL_SCOPE" | "RENOX_TOOL_CONFIRM";
16
+ export type RenoxErrorCode = (typeof RENOX_ERROR_CODES)[number];
17
+ /** Never reaches tenant code as an exception; clients see it in the error body. */
18
+ export type ResponseErrorCode = (typeof RESPONSE_ERROR_CODES)[number];
19
+ /** Body of a platform error response on an API route (errors.md, "Response body"). */
20
+ export interface ErrorResponseBody {
21
+ error: {
22
+ code: ResponseErrorCode;
23
+ request_id: string;
24
+ message: string;
25
+ };
26
+ }
27
+ export declare class RenoxError extends Error {
28
+ readonly code: RenoxErrorCode;
29
+ readonly details?: Readonly<Record<string, unknown>>;
30
+ constructor(code: RenoxErrorCode, message: string, details?: Record<string, unknown>);
31
+ }
32
+ /**
33
+ * True for any error carrying a known `RenoxErrorCode`. Prefer it over
34
+ * `instanceof`: an error raised by the platform's runtime glue and the class a
35
+ * tenant bundled are not guaranteed to be the same object.
36
+ */
37
+ export declare function isRenoxError(error: unknown): error is RenoxError;
38
+ export declare class DbPermissionError extends RenoxError {
39
+ readonly code: "RENOX_DB_AUTH";
40
+ readonly details: {
41
+ readonly table: string;
42
+ readonly action: string;
43
+ };
44
+ constructor(message: string, details: {
45
+ table: string;
46
+ action: string;
47
+ });
48
+ }
49
+ export declare class DbBusyError extends RenoxError {
50
+ readonly code: "RENOX_DB_BUSY";
51
+ constructor(message: string);
52
+ }
53
+ export declare class DbTimeoutError extends RenoxError {
54
+ readonly code: "RENOX_DB_TIMEOUT";
55
+ constructor(message: string);
56
+ }
57
+ export declare class DbQuotaError extends RenoxError {
58
+ readonly code: "RENOX_DB_QUOTA";
59
+ constructor(message: string);
60
+ }
61
+ export declare class DbConstraintError extends RenoxError {
62
+ readonly code: "RENOX_DB_CONSTRAINT";
63
+ readonly details: {
64
+ readonly kind: "unique" | "foreign_key" | "not_null" | "check" | "primary_key";
65
+ };
66
+ constructor(message: string, details: {
67
+ kind: "unique" | "foreign_key" | "not_null" | "check" | "primary_key";
68
+ });
69
+ }
70
+ export declare class DbSqlError extends RenoxError {
71
+ readonly code: "RENOX_DB_SQL";
72
+ constructor(message: string);
73
+ }
74
+ export type DbUsageErrorCode = "RENOX_DB_PARAM" | "RENOX_DB_COLUMN_NAME" | "RENOX_DB_MULTI_STATEMENT" | "RENOX_DB_RESULT_LIMIT" | "RENOX_DB_TX_STATE";
75
+ export declare class DbUsageError extends RenoxError {
76
+ readonly code: DbUsageErrorCode;
77
+ constructor(code: DbUsageErrorCode, message: string);
78
+ }
79
+ export type DbScopeReason = "unscoped" | "scope_missing" | "owner_mismatch" | "unprovable";
80
+ /** Module code reached an owned table without a scope, or the rewriter refused the statement (ops.md section 17). */
81
+ export declare class DbScopeError extends RenoxError {
82
+ readonly code: "RENOX_DB_SCOPE";
83
+ readonly details: {
84
+ readonly table: string;
85
+ readonly module: string;
86
+ readonly reason: DbScopeReason;
87
+ };
88
+ constructor(message: string, details: {
89
+ table: string;
90
+ module: string;
91
+ reason: DbScopeReason;
92
+ });
93
+ }
94
+ export type FetchBlockedReason = "not_allowlisted" | "scheme" | "port" | "blocked_address" | "redirect_limit" | "method";
95
+ export type FetchFailedReason = "dns" | "connect" | "tls" | "reset" | "redirect" | "concurrency" | "headers";
96
+ export declare class FetchBlockedError extends RenoxError {
97
+ readonly code: "RENOX_FETCH_BLOCKED";
98
+ readonly details: {
99
+ readonly host: string;
100
+ readonly reason: FetchBlockedReason;
101
+ };
102
+ constructor(message: string, details: {
103
+ host: string;
104
+ reason: FetchBlockedReason;
105
+ });
106
+ }
107
+ export declare class FetchError extends RenoxError {
108
+ readonly code: "RENOX_FETCH_FAILED";
109
+ readonly details: {
110
+ readonly host: string;
111
+ readonly reason: FetchFailedReason;
112
+ };
113
+ constructor(message: string, details: {
114
+ host: string;
115
+ reason: FetchFailedReason;
116
+ });
117
+ }
118
+ export declare class FetchEgressLimitError extends RenoxError {
119
+ readonly code: "RENOX_FETCH_EGRESS_LIMIT";
120
+ readonly details: {
121
+ readonly host: string;
122
+ readonly limitBytes: number;
123
+ };
124
+ constructor(message: string, details: {
125
+ host: string;
126
+ limitBytes: number;
127
+ });
128
+ }
129
+ export declare class DeadlineError extends RenoxError {
130
+ readonly code: "RENOX_DEADLINE";
131
+ constructor(message: string);
132
+ }
133
+ /** A service call, hook run, or event emit the caller's manifest does not allow. */
134
+ export declare class ModuleUndeclaredError extends RenoxError {
135
+ readonly code: "RENOX_MODULE_UNDECLARED";
136
+ readonly details: {
137
+ readonly module: string;
138
+ readonly target: string;
139
+ };
140
+ constructor(message: string, details: {
141
+ module: string;
142
+ target: string;
143
+ });
144
+ }
145
+ export type ModuleServiceErrorDetails = {
146
+ readonly module: string;
147
+ readonly service?: string;
148
+ readonly hook?: string;
149
+ readonly event?: string;
150
+ readonly cause_code: string | null;
151
+ };
152
+ /**
153
+ * A service, hook filler, or in-memory event handler of another module threw.
154
+ * `module` names the callee, `service` the service (null for a hook filler or
155
+ * an event handler). The enclosing transaction has been rolled back.
156
+ */
157
+ export declare class ModuleServiceError extends RenoxError {
158
+ readonly code: "RENOX_MODULE_SERVICE";
159
+ readonly details: ModuleServiceErrorDetails;
160
+ readonly module: string;
161
+ readonly service: string | null;
162
+ constructor(message: string, details: ModuleServiceErrorDetails);
163
+ }
164
+ export type EventPayloadReason = "schema" | "not_json" | "too_large";
165
+ /** The payload does not match the event's exported schema; never carries the payload. */
166
+ export declare class EventPayloadError extends RenoxError {
167
+ readonly code: "RENOX_EVENT_PAYLOAD";
168
+ readonly details: {
169
+ readonly event: string;
170
+ readonly reason: EventPayloadReason;
171
+ };
172
+ constructor(message: string, details: {
173
+ event: string;
174
+ reason: EventPayloadReason;
175
+ });
176
+ }
177
+ export type JobErrorCode = "RENOX_JOB_NOT_FOUND" | "RENOX_JOB_STATE";
178
+ export type JobOp = "retry" | "cancel";
179
+ /** `op_jobs_retry` / `op_jobs_cancel` for an id that is not a job row of this tenant. */
180
+ export declare class JobNotFoundError extends RenoxError {
181
+ readonly code: "RENOX_JOB_NOT_FOUND";
182
+ readonly details: {
183
+ readonly id: string;
184
+ };
185
+ constructor(message: string, details: {
186
+ id: string;
187
+ });
188
+ }
189
+ /** Retry on a row that is not dead, or cancel on a running row. */
190
+ export declare class JobStateError extends RenoxError {
191
+ readonly code: "RENOX_JOB_STATE";
192
+ readonly details: {
193
+ readonly id: string;
194
+ readonly state: JobState;
195
+ readonly op: JobOp;
196
+ };
197
+ constructor(message: string, details: {
198
+ id: string;
199
+ state: JobState;
200
+ op: JobOp;
201
+ });
202
+ }
203
+ export type StorageErrorCode = "RENOX_STORAGE_QUOTA" | "RENOX_STORAGE_TYPE" | "RENOX_STORAGE_TOO_LARGE" | "RENOX_STORAGE_KEY" | "RENOX_STORAGE_NOT_FOUND";
204
+ export type AiErrorCode = "RENOX_AI_QUOTA" | "RENOX_AI_UNAVAILABLE";
205
+ /** One of the storage refusals (section 21). */
206
+ export declare class StorageError extends RenoxError {
207
+ readonly code: StorageErrorCode;
208
+ constructor(code: StorageErrorCode, message: string, details?: Record<string, unknown>);
209
+ }
210
+ /** The tenant's monthly AI tokens are spent (section 23). */
211
+ export declare class AiQuotaError extends RenoxError {
212
+ readonly code: "RENOX_AI_QUOTA";
213
+ constructor(message: string, details?: Record<string, unknown>);
214
+ }
215
+ /** The model provider or the platform gateway failed (section 23). */
216
+ export declare class AiUnavailableError extends RenoxError {
217
+ readonly code: "RENOX_AI_UNAVAILABLE";
218
+ constructor(message: string, details?: Record<string, unknown>);
219
+ }
220
+ /** A PDF over 200 pages or 20 MB (section 24). */
221
+ export declare class PdfLimitError extends RenoxError {
222
+ readonly code: "RENOX_PDF_LIMIT";
223
+ constructor(message: string, details?: Record<string, unknown>);
224
+ }
225
+ export type ForbiddenReason = "deny" | "unknown_action" | "missing_attribute" | "evaluation_error" | "eval_limit" | "dev_only";
226
+ export type ForbiddenErrorDetails = {
227
+ readonly action: string;
228
+ readonly resource: {
229
+ readonly type: string;
230
+ readonly id: string;
231
+ } | null;
232
+ readonly reason: ForbiddenReason;
233
+ /** The deciding policy id in dev mode only. */
234
+ readonly policy: string | null;
235
+ };
236
+ /** A policy denied the action. Uncaught, it ends the request with 403 `RENOX_FORBIDDEN`, not 500. */
237
+ export declare class ForbiddenError extends RenoxError {
238
+ readonly code: "RENOX_FORBIDDEN";
239
+ readonly details: ForbiddenErrorDetails;
240
+ constructor(message: string, details: ForbiddenErrorDetails);
241
+ }
242
+ export type RealtimeLimitReason = "tenant" | "user" | "topics" | "message_size" | "flood";
243
+ export declare class RealtimeLimitError extends RenoxError {
244
+ readonly code: "RENOX_REALTIME_LIMIT";
245
+ readonly details: {
246
+ readonly reason: RealtimeLimitReason;
247
+ };
248
+ constructor(message: string, details: {
249
+ reason: RealtimeLimitReason;
250
+ });
251
+ }
252
+ /** The connection's outbound queue was full: it has been closed with 1008. */
253
+ export declare class RealtimeBackpressureError extends RenoxError {
254
+ readonly code: "RENOX_REALTIME_BACKPRESSURE";
255
+ readonly details: {
256
+ readonly conn: string;
257
+ };
258
+ constructor(message: string, details: {
259
+ conn: string;
260
+ });
261
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,232 @@
1
+ // Error codes and classes of docs/contracts/ops.md section 13 (contract
2
+ // version 1.6.0). tests/errors.test.ts checks the lists below against the
3
+ // table in ops.md; tests/contract.test-d.ts checks the types against ctx.d.ts.
4
+ /** Codes an op rejects with inside tenant code ("thrown" and "both" in ops.md section 13). */
5
+ export const RENOX_ERROR_CODES = [
6
+ "RENOX_DB_AUTH",
7
+ "RENOX_DB_BUSY",
8
+ "RENOX_DB_TIMEOUT",
9
+ "RENOX_DB_QUOTA",
10
+ "RENOX_DB_CONSTRAINT",
11
+ "RENOX_DB_SQL",
12
+ "RENOX_DB_PARAM",
13
+ "RENOX_DB_COLUMN_NAME",
14
+ "RENOX_DB_MULTI_STATEMENT",
15
+ "RENOX_DB_RESULT_LIMIT",
16
+ "RENOX_DB_TX_STATE",
17
+ // ops 1.5.0: an owned table without a scope (section 17).
18
+ "RENOX_DB_SCOPE",
19
+ "RENOX_FETCH_BLOCKED",
20
+ "RENOX_FETCH_FAILED",
21
+ "RENOX_FETCH_EGRESS_LIMIT",
22
+ "RENOX_DEADLINE",
23
+ "RENOX_NO_REQUEST",
24
+ "RENOX_SECRETS_CONSUMED",
25
+ "RENOX_MODULE_UNDECLARED",
26
+ "RENOX_MODULE_SERVICE",
27
+ "RENOX_EVENT_PAYLOAD",
28
+ // ops 1.5.0: a policy denied (section 17); the realtime limits (section 18).
29
+ "RENOX_FORBIDDEN",
30
+ "RENOX_REALTIME_LIMIT",
31
+ "RENOX_REALTIME_BACKPRESSURE",
32
+ // ops 1.6.0: the jobs module's ops (section 20).
33
+ "RENOX_JOB_NOT_FOUND",
34
+ "RENOX_JOB_STATE",
35
+ // ops 1.7.0: storage (section 21), AI (section 23), PDF (section 24).
36
+ "RENOX_STORAGE_QUOTA",
37
+ "RENOX_STORAGE_TYPE",
38
+ "RENOX_STORAGE_TOO_LARGE",
39
+ "RENOX_STORAGE_KEY",
40
+ "RENOX_STORAGE_NOT_FOUND",
41
+ "RENOX_AI_QUOTA",
42
+ "RENOX_AI_UNAVAILABLE",
43
+ "RENOX_PDF_LIMIT",
44
+ ];
45
+ /** Codes the platform answers with when a request ends ("response" and "both"). */
46
+ export const RESPONSE_ERROR_CODES = [
47
+ "RENOX_DEADLINE",
48
+ "RENOX_CPU_LIMIT",
49
+ "RENOX_OOM",
50
+ "RENOX_BUSY",
51
+ "RENOX_INTERNAL",
52
+ "RENOX_BODY_TOO_LARGE",
53
+ "RENOX_CSRF",
54
+ "RENOX_RENDER_LIMIT",
55
+ // ops 1.4.0: a lazy route module failed its first evaluation.
56
+ "RENOX_MODULE_EVAL",
57
+ // ops 1.5.0: a guard or the tool check denied; the realtime caps; /mcp refusals (sections 17-19).
58
+ "RENOX_FORBIDDEN",
59
+ "RENOX_REALTIME_LIMIT",
60
+ "RENOX_REALTIME_BACKPRESSURE",
61
+ "RENOX_TOOL_INPUT",
62
+ "RENOX_TOOL_SCOPE",
63
+ "RENOX_TOOL_CONFIRM",
64
+ // ops 1.7.0: the image transform rate (section 21), a refused webhook delivery (section 22).
65
+ "RENOX_IMAGE_RATE",
66
+ "RENOX_WEBHOOK_SIGNATURE",
67
+ ];
68
+ export class RenoxError extends Error {
69
+ code;
70
+ details;
71
+ constructor(code, message, details) {
72
+ super(message);
73
+ this.name = new.target.name;
74
+ this.code = code;
75
+ if (details !== undefined)
76
+ this.details = Object.freeze({ ...details });
77
+ }
78
+ }
79
+ /**
80
+ * True for any error carrying a known `RenoxErrorCode`. Prefer it over
81
+ * `instanceof`: an error raised by the platform's runtime glue and the class a
82
+ * tenant bundled are not guaranteed to be the same object.
83
+ */
84
+ export function isRenoxError(error) {
85
+ if (!(error instanceof Error))
86
+ return false;
87
+ const code = error.code;
88
+ return typeof code === "string" && RENOX_ERROR_CODES.includes(code);
89
+ }
90
+ // Database (ops.md section 1)
91
+ export class DbPermissionError extends RenoxError {
92
+ constructor(message, details) {
93
+ super("RENOX_DB_AUTH", message, details);
94
+ }
95
+ }
96
+ export class DbBusyError extends RenoxError {
97
+ constructor(message) {
98
+ super("RENOX_DB_BUSY", message);
99
+ }
100
+ }
101
+ export class DbTimeoutError extends RenoxError {
102
+ constructor(message) {
103
+ super("RENOX_DB_TIMEOUT", message);
104
+ }
105
+ }
106
+ export class DbQuotaError extends RenoxError {
107
+ constructor(message) {
108
+ super("RENOX_DB_QUOTA", message);
109
+ }
110
+ }
111
+ export class DbConstraintError extends RenoxError {
112
+ constructor(message, details) {
113
+ super("RENOX_DB_CONSTRAINT", message, details);
114
+ }
115
+ }
116
+ export class DbSqlError extends RenoxError {
117
+ constructor(message) {
118
+ super("RENOX_DB_SQL", message);
119
+ }
120
+ }
121
+ export class DbUsageError extends RenoxError {
122
+ constructor(code, message) {
123
+ super(code, message);
124
+ }
125
+ }
126
+ /** Module code reached an owned table without a scope, or the rewriter refused the statement (ops.md section 17). */
127
+ export class DbScopeError extends RenoxError {
128
+ constructor(message, details) {
129
+ super("RENOX_DB_SCOPE", message, details);
130
+ }
131
+ }
132
+ export class FetchBlockedError extends RenoxError {
133
+ constructor(message, details) {
134
+ super("RENOX_FETCH_BLOCKED", message, details);
135
+ }
136
+ }
137
+ export class FetchError extends RenoxError {
138
+ constructor(message, details) {
139
+ super("RENOX_FETCH_FAILED", message, details);
140
+ }
141
+ }
142
+ export class FetchEgressLimitError extends RenoxError {
143
+ constructor(message, details) {
144
+ super("RENOX_FETCH_EGRESS_LIMIT", message, details);
145
+ }
146
+ }
147
+ // Deadline (ops.md section 5)
148
+ export class DeadlineError extends RenoxError {
149
+ constructor(message) {
150
+ super("RENOX_DEADLINE", message);
151
+ }
152
+ }
153
+ // Modules (ops.md section 15)
154
+ /** A service call, hook run, or event emit the caller's manifest does not allow. */
155
+ export class ModuleUndeclaredError extends RenoxError {
156
+ constructor(message, details) {
157
+ super("RENOX_MODULE_UNDECLARED", message, details);
158
+ }
159
+ }
160
+ /**
161
+ * A service, hook filler, or in-memory event handler of another module threw.
162
+ * `module` names the callee, `service` the service (null for a hook filler or
163
+ * an event handler). The enclosing transaction has been rolled back.
164
+ */
165
+ export class ModuleServiceError extends RenoxError {
166
+ module;
167
+ service;
168
+ constructor(message, details) {
169
+ super("RENOX_MODULE_SERVICE", message, { ...details });
170
+ this.module = details.module;
171
+ this.service = details.service ?? null;
172
+ }
173
+ }
174
+ /** The payload does not match the event's exported schema; never carries the payload. */
175
+ export class EventPayloadError extends RenoxError {
176
+ constructor(message, details) {
177
+ super("RENOX_EVENT_PAYLOAD", message, details);
178
+ }
179
+ }
180
+ /** `op_jobs_retry` / `op_jobs_cancel` for an id that is not a job row of this tenant. */
181
+ export class JobNotFoundError extends RenoxError {
182
+ constructor(message, details) {
183
+ super("RENOX_JOB_NOT_FOUND", message, details);
184
+ }
185
+ }
186
+ /** Retry on a row that is not dead, or cancel on a running row. */
187
+ export class JobStateError extends RenoxError {
188
+ constructor(message, details) {
189
+ super("RENOX_JOB_STATE", message, details);
190
+ }
191
+ }
192
+ /** One of the storage refusals (section 21). */
193
+ export class StorageError extends RenoxError {
194
+ constructor(code, message, details) {
195
+ super(code, message, details);
196
+ }
197
+ }
198
+ /** The tenant's monthly AI tokens are spent (section 23). */
199
+ export class AiQuotaError extends RenoxError {
200
+ constructor(message, details) {
201
+ super("RENOX_AI_QUOTA", message, details);
202
+ }
203
+ }
204
+ /** The model provider or the platform gateway failed (section 23). */
205
+ export class AiUnavailableError extends RenoxError {
206
+ constructor(message, details) {
207
+ super("RENOX_AI_UNAVAILABLE", message, details);
208
+ }
209
+ }
210
+ /** A PDF over 200 pages or 20 MB (section 24). */
211
+ export class PdfLimitError extends RenoxError {
212
+ constructor(message, details) {
213
+ super("RENOX_PDF_LIMIT", message, details);
214
+ }
215
+ }
216
+ /** A policy denied the action. Uncaught, it ends the request with 403 `RENOX_FORBIDDEN`, not 500. */
217
+ export class ForbiddenError extends RenoxError {
218
+ constructor(message, details) {
219
+ super("RENOX_FORBIDDEN", message, { ...details });
220
+ }
221
+ }
222
+ export class RealtimeLimitError extends RenoxError {
223
+ constructor(message, details) {
224
+ super("RENOX_REALTIME_LIMIT", message, details);
225
+ }
226
+ }
227
+ /** The connection's outbound queue was full: it has been closed with 1008. */
228
+ export class RealtimeBackpressureError extends RenoxError {
229
+ constructor(message, details) {
230
+ super("RENOX_REALTIME_BACKPRESSURE", message, details);
231
+ }
232
+ }
@@ -0,0 +1,35 @@
1
+ import type { Ai, AiRequest, Storage, StoredFile, WebhookProvider } from "./ctx.ts";
2
+ /** An in-memory `ctx.storage` for `module` (keys under `<module>/`). */
3
+ export declare function fakeStorage(module?: string, options?: {
4
+ maxFileBytes?: number;
5
+ now?: () => number;
6
+ }): Storage & {
7
+ files: Map<string, {
8
+ file: StoredFile;
9
+ bytes: Uint8Array;
10
+ }>;
11
+ };
12
+ /** One recorded answer: text, or a tool call the code under test must answer. */
13
+ export type RecordedTurn = {
14
+ text: string;
15
+ } | {
16
+ tool: string;
17
+ input?: unknown;
18
+ };
19
+ /**
20
+ * A `ctx.ai` that replays recorded answers in order, running local tools
21
+ * like the platform does; `requests` keeps what the code asked.
22
+ */
23
+ export declare function fakeAi(turns: RecordedTurn[]): Ai & {
24
+ requests: AiRequest[];
25
+ };
26
+ /**
27
+ * A webhook delivery signed the way `provider` signs it (ops.md section 22),
28
+ * for a test of a `functions/webhooks/*.ts` handler against `renox test`.
29
+ * `generic` signs into `header` (default `x-webhook-signature`).
30
+ */
31
+ export declare function signWebhook(provider: Exclude<WebhookProvider, "apple" | "google">, secret: string, payload: unknown, options?: {
32
+ url?: string;
33
+ header?: string;
34
+ now?: number;
35
+ }): Promise<Request>;
package/dist/fakes.js ADDED
@@ -0,0 +1,144 @@
1
+ // `@renoxium/sdk/fakes` (#323; docs/contracts/ops.md 1.7.0 sections 21-23): stand-ins
2
+ // for `ctx.storage`, `ctx.ai` and webhook deliveries in a tenant's unit
3
+ // tests, so a handler can be called without a worker.
4
+ //
5
+ // import { fakeStorage, fakeAi, signWebhook } from "@renoxium/sdk/fakes";
6
+ // const ctx = { ...base, storage: fakeStorage(), ai: fakeAi([{ text: "AED 5" }]) };
7
+ // const req = await signWebhook("stripe", "whsec_test", { id: "evt_1" });
8
+ //
9
+ // The fakes follow the contract's rules (key prefix, ttl range, the
10
+ // `StorageError` codes), never the network.
11
+ import { StorageError } from "./errors.js";
12
+ const KEY = /^[a-z0-9][a-z0-9/_.-]{0,255}$/u;
13
+ async function sha256Hex(bytes) {
14
+ const d = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
15
+ return [...d].map((b) => b.toString(16).padStart(2, "0")).join("");
16
+ }
17
+ /** An in-memory `ctx.storage` for `module` (keys under `<module>/`). */
18
+ export function fakeStorage(module = "app", options = {}) {
19
+ const files = new Map();
20
+ const now = options.now ?? Date.now;
21
+ const check = (key) => {
22
+ if (!KEY.test(key) || key.split("/").some((s) => s === "" || s === "." || s === "..") || !key.startsWith(`${module}/`)) {
23
+ throw new StorageError("RENOX_STORAGE_KEY", "The storage key is not allowed.", { key });
24
+ }
25
+ };
26
+ const found = (key) => {
27
+ const f = files.get(key);
28
+ if (f === undefined)
29
+ throw new StorageError("RENOX_STORAGE_NOT_FOUND", "No stored file has this key.", { key });
30
+ return f;
31
+ };
32
+ return {
33
+ files,
34
+ async put(key, bytes, opts) {
35
+ check(key);
36
+ if (bytes.length > (options.maxFileBytes ?? 10 * 1024 * 1024))
37
+ throw new StorageError("RENOX_STORAGE_TOO_LARGE", "The file is over the per-file limit.", { key });
38
+ if (opts.contentType === "image/svg+xml" && opts.sanitised !== true)
39
+ throw new StorageError("RENOX_STORAGE_TYPE", "The content type is not allowed for this file.", { key, reason: "svg_unsanitised" });
40
+ const file = { key, bytes: bytes.length, content_type: opts.contentType, sha256: await sha256Hex(bytes), width: null, height: null, public: opts.public === true, created_at: now() };
41
+ files.set(key, { file, bytes: bytes.slice() });
42
+ return file;
43
+ },
44
+ async get(key) {
45
+ check(key);
46
+ return found(key).file;
47
+ },
48
+ async read(key) {
49
+ check(key);
50
+ return found(key).bytes.slice();
51
+ },
52
+ async delete(key) {
53
+ check(key);
54
+ return files.delete(key);
55
+ },
56
+ async sign(key, ttlSeconds) {
57
+ check(key);
58
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > 604800)
59
+ throw new RangeError("storage.sign: ttl is 60 to 604800 seconds");
60
+ found(key);
61
+ const exp = Math.floor(now() / 1000) + ttlSeconds;
62
+ return { url: `/_files/${key}?exp=${exp}&sig=fake`, expires_at: exp * 1000 };
63
+ },
64
+ async list(prefix, opts = {}) {
65
+ const p = prefix === "" ? `${module}/` : prefix;
66
+ if (!p.startsWith(`${module}/`))
67
+ throw new StorageError("RENOX_STORAGE_KEY", "The storage key is not allowed.", { key: p });
68
+ const keys = [...files.keys()].filter((k) => k.startsWith(p) && k > (opts.cursor ?? "")).sort();
69
+ const limit = opts.limit ?? 100;
70
+ const page = keys.slice(0, limit);
71
+ return { items: page.map((k) => files.get(k).file), next_cursor: keys.length > limit ? (page[page.length - 1] ?? null) : null };
72
+ },
73
+ };
74
+ }
75
+ /**
76
+ * A `ctx.ai` that replays recorded answers in order, running local tools
77
+ * like the platform does; `requests` keeps what the code asked.
78
+ */
79
+ export function fakeAi(turns) {
80
+ const requests = [];
81
+ let i = 0;
82
+ const next = () => {
83
+ const t = turns[i++];
84
+ if (t === undefined)
85
+ throw new Error("fakeAi: no recorded answer left");
86
+ return t;
87
+ };
88
+ return {
89
+ requests,
90
+ async chat(request) {
91
+ requests.push(request);
92
+ let input = 0;
93
+ let output = 0;
94
+ for (let round = 0; round < 8; round++) {
95
+ const t = next();
96
+ input += 10;
97
+ output += 5;
98
+ if ("text" in t) {
99
+ return { id: `fake_${i}`, model: request.model ?? "default", content: [{ type: "text", text: t.text }], stop_reason: "end_turn", usage: { input_tokens: input, output_tokens: output } };
100
+ }
101
+ const tool = request.tools?.find((x) => x.name === t.tool);
102
+ if (typeof tool?.run !== "function") {
103
+ const content = [{ type: "tool_use", id: `tu_${i}`, name: t.tool, input: (t.input ?? {}) }];
104
+ return { id: `fake_${i}`, model: request.model ?? "default", content, stop_reason: "tool_use", usage: { input_tokens: input, output_tokens: output } };
105
+ }
106
+ await tool.run((t.input ?? {}));
107
+ }
108
+ throw new Error("fakeAi: more than 8 tool rounds");
109
+ },
110
+ };
111
+ }
112
+ const HEADERS = {
113
+ checkout: "cko-signature",
114
+ telr: "x-telr-signature",
115
+ paytabs: "signature",
116
+ ngenius: "x-ngenius-signature",
117
+ tabby: "x-tabby-signature",
118
+ tamara: "tamara-signature",
119
+ };
120
+ async function hmacHex(secret, data) {
121
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
122
+ return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, data))].map((b) => b.toString(16).padStart(2, "0")).join("");
123
+ }
124
+ /**
125
+ * A webhook delivery signed the way `provider` signs it (ops.md section 22),
126
+ * for a test of a `functions/webhooks/*.ts` handler against `renox test`.
127
+ * `generic` signs into `header` (default `x-webhook-signature`).
128
+ */
129
+ export async function signWebhook(provider, secret, payload, options = {}) {
130
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
131
+ const bytes = new TextEncoder().encode(body);
132
+ const headers = { "content-type": "application/json" };
133
+ if (provider === "stripe") {
134
+ const t = Math.floor((options.now ?? Date.now()) / 1000);
135
+ headers["stripe-signature"] = `t=${t},v1=${await hmacHex(secret, new TextEncoder().encode(`${t}.${body}`))}`;
136
+ }
137
+ else if (provider === "generic") {
138
+ headers[(options.header ?? "x-webhook-signature").toLowerCase()] = await hmacHex(secret, bytes);
139
+ }
140
+ else {
141
+ headers[HEADERS[provider]] = await hmacHex(secret, bytes);
142
+ }
143
+ return new Request(options.url ?? `https://app.renox.test/webhooks/${provider}`, { method: "POST", headers, body });
144
+ }