@zveltio/sdk 3.0.0-beta.4 → 3.0.0-beta.68

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.
Files changed (101) hide show
  1. package/dist/client/Auth.d.ts.map +1 -1
  2. package/dist/client/Auth.js +3 -0
  3. package/dist/client/QueryBuilder.d.ts.map +1 -1
  4. package/dist/client/QueryBuilder.js +4 -0
  5. package/dist/client/ZveltioClient.d.ts.map +1 -1
  6. package/dist/client/ZveltioClient.js +5 -0
  7. package/dist/client.d.ts +4 -1
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +30 -13
  10. package/dist/core.d.ts.map +1 -1
  11. package/dist/core.js +21 -3
  12. package/dist/crdt.d.ts.map +1 -1
  13. package/dist/crdt.js +11 -2
  14. package/dist/ddl/index.d.ts.map +1 -1
  15. package/dist/errors.d.ts +58 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +124 -0
  18. package/dist/extension/index.d.ts +419 -19
  19. package/dist/extension/index.d.ts.map +1 -1
  20. package/dist/extension/index.js +3 -0
  21. package/dist/extension/jsonb.d.ts +62 -0
  22. package/dist/extension/jsonb.d.ts.map +1 -0
  23. package/dist/extension/jsonb.js +64 -0
  24. package/dist/extension/multipart.d.ts +58 -0
  25. package/dist/extension/multipart.d.ts.map +1 -0
  26. package/dist/extension/multipart.js +61 -0
  27. package/dist/extension/numeric.d.ts +94 -0
  28. package/dist/extension/numeric.d.ts.map +1 -0
  29. package/dist/extension/numeric.js +162 -0
  30. package/dist/extension/permission-gate.d.ts +15 -0
  31. package/dist/extension/permission-gate.d.ts.map +1 -1
  32. package/dist/extension/permission-gate.js +33 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/local-store.d.ts +5 -0
  37. package/dist/local-store.d.ts.map +1 -1
  38. package/dist/local-store.js +62 -10
  39. package/dist/offline/index.d.ts +3 -3
  40. package/dist/offline/index.js +51 -15
  41. package/dist/publish/index.d.ts.map +1 -1
  42. package/dist/publish/index.js +6 -1
  43. package/dist/realtime.d.ts +9 -1
  44. package/dist/realtime.d.ts.map +1 -1
  45. package/dist/realtime.js +15 -2
  46. package/dist/rpc/index.d.ts.map +1 -1
  47. package/dist/rpc/index.js +2 -0
  48. package/dist/schema-codegen.d.ts +3 -1
  49. package/dist/schema-codegen.d.ts.map +1 -1
  50. package/dist/schema-codegen.js +41 -7
  51. package/dist/schema-watcher.d.ts +1 -1
  52. package/dist/schema-watcher.js +1 -1
  53. package/dist/studio/index.d.ts +1 -0
  54. package/dist/studio/index.d.ts.map +1 -1
  55. package/dist/studio/index.js +3 -0
  56. package/dist/studio/slot-owner-registry.d.ts +13 -0
  57. package/dist/studio/slot-owner-registry.d.ts.map +1 -0
  58. package/dist/studio/slot-owner-registry.js +21 -0
  59. package/dist/svelte.d.ts +1 -1
  60. package/dist/svelte.d.ts.map +1 -1
  61. package/dist/svelte.js +4 -2
  62. package/dist/sync-manager.d.ts +30 -4
  63. package/dist/sync-manager.d.ts.map +1 -1
  64. package/dist/sync-manager.js +101 -15
  65. package/dist/testing/index.d.ts.map +1 -1
  66. package/dist/testing/index.js +23 -1
  67. package/dist/testing/with-test-db.d.ts.map +1 -1
  68. package/dist/testing/with-test-db.js +21 -14
  69. package/dist/tests/client.test.d.ts +2 -0
  70. package/dist/tests/client.test.d.ts.map +1 -0
  71. package/dist/tests/client.test.js +55 -0
  72. package/dist/tests/crdt-orset.test.d.ts +2 -0
  73. package/dist/tests/crdt-orset.test.d.ts.map +1 -0
  74. package/dist/tests/crdt-orset.test.js +23 -0
  75. package/dist/tests/local-store.test.js +44 -0
  76. package/dist/tests/multipart.test.d.ts +2 -0
  77. package/dist/tests/multipart.test.d.ts.map +1 -0
  78. package/dist/tests/multipart.test.js +37 -0
  79. package/dist/tests/offline-provider-crdt.test.d.ts +2 -0
  80. package/dist/tests/offline-provider-crdt.test.d.ts.map +1 -0
  81. package/dist/tests/offline-provider-crdt.test.js +119 -0
  82. package/dist/tests/realtime.test.d.ts +2 -0
  83. package/dist/tests/realtime.test.d.ts.map +1 -0
  84. package/dist/tests/realtime.test.js +39 -0
  85. package/dist/tests/sync-manager.test.d.ts +2 -0
  86. package/dist/tests/sync-manager.test.d.ts.map +1 -0
  87. package/dist/tests/sync-manager.test.js +65 -0
  88. package/dist/tests/with-test-db.test.d.ts +2 -0
  89. package/dist/tests/with-test-db.test.d.ts.map +1 -0
  90. package/dist/tests/with-test-db.test.js +57 -0
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/validate/index.d.ts +49 -0
  93. package/dist/validate/index.d.ts.map +1 -1
  94. package/dist/validate/index.js +494 -7
  95. package/dist/validate/shared-message-keys.d.ts +16 -0
  96. package/dist/validate/shared-message-keys.d.ts.map +1 -0
  97. package/dist/validate/shared-message-keys.js +227 -0
  98. package/package.json +7 -7
  99. package/dist/generate-types.d.ts +0 -37
  100. package/dist/generate-types.d.ts.map +0 -1
  101. package/dist/generate-types.js +0 -157
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Binding a JavaScript value to a `jsonb` column.
3
+ *
4
+ * There is one correct form and three plausible wrong ones. All four were
5
+ * measured against this driver, on a real `jsonb` column:
6
+ *
7
+ * JSON.stringify(v) → a jsonb STRING. `jsonb_typeof` says
8
+ * `string`, `col->>'k'` is NULL and
9
+ * `col ? 'k'` is false.
10
+ *
11
+ * the raw value → correct for an object, WRONG for an
12
+ * array: the driver renders a JS array
13
+ * as a Postgres array literal, so
14
+ * `[{a:1}]` lands as the string
15
+ * `{"[object Object]"}`.
16
+ *
17
+ * JSON.stringify(v) + `::jsonb` → still a jsonb STRING. The obvious
18
+ * repair and a trap: the driver has
19
+ * already encoded the parameter as JSON,
20
+ * so the cast turns a jsonb string into
21
+ * a jsonb string.
22
+ *
23
+ * JSON.stringify(v) + `::text::jsonb` → correct, and correct for EVERY type —
24
+ * object, array, string, number, boolean
25
+ * and null. `::text` forces the parameter
26
+ * to be read as text first, and the
27
+ * second cast parses it.
28
+ *
29
+ * ── Why this lives in the SDK ─────────────────────────────────────
30
+ *
31
+ * Because it had already been written twice. The engine has `lib/jsonb.ts`, and
32
+ * `content/pages` wrote its own after the defect surfaced there in the way that
33
+ * hurts: a popup's `targets` were appended with `jsonb ||`, and the result was
34
+ * an ARRAY CONTAINING THE OLD TEXT, because the old value was text. Seventeen
35
+ * more extensions were writing the same shape with no helper at all.
36
+ *
37
+ * A third copy per extension would be the repository's own dominant bug shape —
38
+ * one rule, written out again somewhere else with a piece missing — so it goes
39
+ * where both sides already compile against: `@zveltio/sdk/extension`.
40
+ *
41
+ * ── Why the defect is invisible until it is not ───────────────────
42
+ *
43
+ * Every reader in both repositories carries `typeof x === 'string' ?
44
+ * JSON.parse(x) : x`, so a string-scalar column reads back correctly and the
45
+ * bug hides. It surfaces the moment SQL treats the column as structured — a
46
+ * `->>`, an index, a `||`, a `jsonb_array_elements`. Two of those have already
47
+ * happened: the `content/pages` popup above, and the engine's flow scheduler,
48
+ * which read `trigger_config.interval_seconds` off a string and therefore ran
49
+ * every cron flow at the default interval instead of its configured one.
50
+ *
51
+ * `scripts/check-jsonb-binding.ts` in the engine repository is the ratchet.
52
+ */
53
+ import { sql } from 'kysely';
54
+ /**
55
+ * A value ready to be assigned to a `jsonb` column.
56
+ *
57
+ * Works in `.values()`, in `.set()` and in a `doUpdateSet()` branch — all three
58
+ * are writers of the same column and all three have been wrong at some point.
59
+ *
60
+ * await db.insertInto('zv_forms').values({ fields: toJsonb(fields) }).execute();
61
+ */
62
+ export function toJsonb(value) {
63
+ return sql `${JSON.stringify(value ?? null)}::text::jsonb`;
64
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Reading a multipart body without turning a bad request into a crash.
3
+ *
4
+ * Six extensions opened an upload handler with
5
+ *
6
+ * const formData = await c.req.formData();
7
+ * const file = formData.get('file') as File;
8
+ * if (!file) return c.json({ error: 'No file provided' }, 400);
9
+ *
10
+ * The 400 on the third line says plainly what the author intended a malformed
11
+ * request to get. It is unreachable: `formData()` throws when the body is not
12
+ * multipart at all — a client that sends JSON, or sends nothing, or gets the
13
+ * content-type wrong — so those requests never reach line three. They surface as
14
+ * an unhandled rejection and a bare 500.
15
+ *
16
+ * That is worth more than tidiness. A 500 tells a caller, a retrying webhook
17
+ * sender, and an uptime monitor that the SERVER is broken, so the client keeps
18
+ * resending a request that can never succeed and the operator is paged for a
19
+ * fault that is not theirs. The same request through this helper gets the 400
20
+ * the handler already meant to send.
21
+ *
22
+ * Deliberately not a middleware: a middleware would have to guess which routes
23
+ * are multipart, and being wrong in either direction is worse than one explicit
24
+ * call per handler.
25
+ */
26
+ /**
27
+ * The body every caller sends with the 400. One string, so the six upload
28
+ * handlers cannot drift into six different ways of saying the same thing.
29
+ */
30
+ export declare const MULTIPART_REQUIRED: {
31
+ readonly error: "Expected a multipart/form-data body.";
32
+ };
33
+ /**
34
+ * Parse a multipart body, answering `null` rather than throwing when it is not one.
35
+ *
36
+ * ```ts
37
+ * const form = await readMultipart(c);
38
+ * if (!form) return c.json(MULTIPART_REQUIRED, 400);
39
+ * const file = form.get('file');
40
+ * ```
41
+ *
42
+ * Returns `FormData | null` rather than a `{ ok }` result object deliberately.
43
+ * The extensions repo compiles with `strict: false`, and without
44
+ * `strictNullChecks` TypeScript will not narrow a discriminated union on a
45
+ * boolean member — every call site got `Property 'error' does not exist on type
46
+ * MultipartResult`. A nullable return needs no narrowing to be safe under either
47
+ * setting, and reads the same.
48
+ *
49
+ * `c` is typed structurally rather than as Hono's `Context` on purpose: the SDK
50
+ * has never taken a dependency on Hono, and every extension bundles its own
51
+ * copy, so a nominal type here would reject the caller's own context object.
52
+ */
53
+ export declare function readMultipart(c: {
54
+ req: {
55
+ formData: () => Promise<FormData>;
56
+ };
57
+ }): Promise<FormData | null>;
58
+ //# sourceMappingURL=multipart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/extension/multipart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;CAA6D,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE;IACrC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAC;CAC5C,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAS3B"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Reading a multipart body without turning a bad request into a crash.
3
+ *
4
+ * Six extensions opened an upload handler with
5
+ *
6
+ * const formData = await c.req.formData();
7
+ * const file = formData.get('file') as File;
8
+ * if (!file) return c.json({ error: 'No file provided' }, 400);
9
+ *
10
+ * The 400 on the third line says plainly what the author intended a malformed
11
+ * request to get. It is unreachable: `formData()` throws when the body is not
12
+ * multipart at all — a client that sends JSON, or sends nothing, or gets the
13
+ * content-type wrong — so those requests never reach line three. They surface as
14
+ * an unhandled rejection and a bare 500.
15
+ *
16
+ * That is worth more than tidiness. A 500 tells a caller, a retrying webhook
17
+ * sender, and an uptime monitor that the SERVER is broken, so the client keeps
18
+ * resending a request that can never succeed and the operator is paged for a
19
+ * fault that is not theirs. The same request through this helper gets the 400
20
+ * the handler already meant to send.
21
+ *
22
+ * Deliberately not a middleware: a middleware would have to guess which routes
23
+ * are multipart, and being wrong in either direction is worse than one explicit
24
+ * call per handler.
25
+ */
26
+ /**
27
+ * The body every caller sends with the 400. One string, so the six upload
28
+ * handlers cannot drift into six different ways of saying the same thing.
29
+ */
30
+ export const MULTIPART_REQUIRED = { error: 'Expected a multipart/form-data body.' };
31
+ /**
32
+ * Parse a multipart body, answering `null` rather than throwing when it is not one.
33
+ *
34
+ * ```ts
35
+ * const form = await readMultipart(c);
36
+ * if (!form) return c.json(MULTIPART_REQUIRED, 400);
37
+ * const file = form.get('file');
38
+ * ```
39
+ *
40
+ * Returns `FormData | null` rather than a `{ ok }` result object deliberately.
41
+ * The extensions repo compiles with `strict: false`, and without
42
+ * `strictNullChecks` TypeScript will not narrow a discriminated union on a
43
+ * boolean member — every call site got `Property 'error' does not exist on type
44
+ * MultipartResult`. A nullable return needs no narrowing to be safe under either
45
+ * setting, and reads the same.
46
+ *
47
+ * `c` is typed structurally rather than as Hono's `Context` on purpose: the SDK
48
+ * has never taken a dependency on Hono, and every extension bundles its own
49
+ * copy, so a nominal type here would reject the caller's own context object.
50
+ */
51
+ export async function readMultipart(c) {
52
+ try {
53
+ return await c.req.formData();
54
+ }
55
+ catch {
56
+ // The parser's own message is not passed through. It reads like "FormData
57
+ // parse error" — nothing the caller can act on — and echoing a body parser's
58
+ // internals back over HTTP is how request contents end up in error strings.
59
+ return null;
60
+ }
61
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Turning what PostgreSQL sends into what JavaScript can add.
3
+ *
4
+ * `BIGINT`, `NUMERIC`, `DECIMAL` and every `COUNT`/`SUM`/`AVG` aggregate arrive
5
+ * from the driver as **strings** — Bun's `SQL` exposes no type-parser hook, so
6
+ * there is no place to fix this once for the whole process. That leaves the call
7
+ * site, and the call site is where it goes wrong:
8
+ *
9
+ * total + row.amount // "0" + "12.50" → "012.50"
10
+ * ("012.50") - 1 // NaN
11
+ *
12
+ * Only `+` misbehaves; `-`, `*`, `/`, `<` and `>` coerce and give correct
13
+ * answers. So the failure hides until a sum happens to enter the chain, and then
14
+ * it does not throw — it produces `NaN`, PostgreSQL accepts `NaN` into a
15
+ * `NUMERIC` column, and `NaN` compares as LARGER than every number. A row poisoned
16
+ * this way passes `WHERE balance > 0`, passes `HAVING total >= required`, and
17
+ * passes a `CHECK (col >= 0)` constraint. The database agrees with the corruption.
18
+ *
19
+ * Hence two rules, both enforced here rather than remembered:
20
+ *
21
+ * * `toNumber` REFUSES `NaN` and `Infinity`. Not "returns 0" — refuses. A value
22
+ * that reached this point non-finite is already the product of a bug, and
23
+ * substituting a plausible number buries it one layer deeper.
24
+ * * `sumNumeric` exists so the common case — adding a column across rows — has
25
+ * an obvious right answer that is shorter to write than the wrong one.
26
+ *
27
+ * This lives in the SDK, not the engine, on purpose. The safe helper existing
28
+ * somewhere an extension cannot import it is a shape this codebase has produced
29
+ * four times already — `safeFetch`, `maybeDecrypt`, `requireInstanceAdmin` and
30
+ * the validation evaluator all had an engine-side implementation and an
31
+ * extension-side reimplementation that was subtly worse. Every module that
32
+ * handles money is an extension, so the conversion belongs where they can reach
33
+ * it: `import { toNumber } from '@zveltio/sdk/extension'`.
34
+ *
35
+ * The engine keeps its own copy at `lib/numeric.ts` rather than importing this
36
+ * one, because the engine's production code has never taken a RUNTIME dependency
37
+ * on the SDK — only types — and a pure sixty-line converter is not the thing to
38
+ * change that for. `numeric-parity.test.ts` runs both against the same table of
39
+ * inputs so the twins cannot drift apart quietly.
40
+ *
41
+ * @see `PgNumeric` in the engine's `db/schema.ts` for the type side of the same problem.
42
+ */
43
+ /** Thrown instead of quietly producing a number nobody can trace. */
44
+ export declare class NumericConversionError extends Error {
45
+ readonly value: unknown;
46
+ constructor(value: unknown, label?: string);
47
+ }
48
+ /**
49
+ * A database numeric as a JS number, or a throw.
50
+ *
51
+ * Accepts what the driver actually produces: a string, a number that survived
52
+ * because the column was `INTEGER`, a `bigint`, or `null`/`undefined` for a
53
+ * nullable column. Everything else, and anything non-finite, is an error.
54
+ *
55
+ * `null` and `undefined` map to `fallback`, which defaults to `0`. That is the
56
+ * one substitution worth making: "no row" and "no value" genuinely do mean zero
57
+ * in an aggregate, and the alternative is a `?? 0` at every call site, which is
58
+ * the shape that lets a real `NaN` through.
59
+ */
60
+ export declare function toNumber(value: unknown, fallback?: number, label?: string): number;
61
+ /**
62
+ * `toNumber` for a value that is allowed to be absent, with no substitution.
63
+ *
64
+ * Use it where zero and "not set" mean different things — a quota that has not
65
+ * been configured is not a quota of zero.
66
+ */
67
+ export declare function toNumberOrNull(value: unknown, label?: string): number | null;
68
+ /**
69
+ * `toNumber` that answers `fallback` instead of throwing.
70
+ *
71
+ * For display paths only — a dashboard tile should not 500 because one row is
72
+ * bad. Never use it on a value about to be written back to the database, which
73
+ * is exactly how the poison spreads.
74
+ */
75
+ export declare function toNumberSafe(value: unknown, fallback?: number): number;
76
+ /** Sum one numeric column across rows, without a `+` on a string in sight. */
77
+ export declare function sumNumeric<T>(rows: readonly T[], pick: (row: T) => unknown, label?: string): number;
78
+ /**
79
+ * Round to a currency's minor unit.
80
+ *
81
+ * Money read as a string and converted to a double is exact for every value a
82
+ * `NUMERIC(14,2)` can hold, but arithmetic on doubles is not: `0.1 + 0.2` is
83
+ * `0.30000000000000004`, and written back that is a `NUMERIC` with fourteen
84
+ * decimal places. Round at the point the result becomes an amount again.
85
+ */
86
+ export declare function roundMoney(value: number, decimals?: number): number;
87
+ /**
88
+ * Is this value safe to store in a `NUMERIC` column?
89
+ *
90
+ * The database will not tell you. `INSERT ... VALUES ('NaN'::numeric)` succeeds,
91
+ * and so does a `CHECK (col >= 0)` on the row afterwards. Guard on the way in.
92
+ */
93
+ export declare function isStorableNumeric(value: unknown): boolean;
94
+ //# sourceMappingURL=numeric.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numeric.d.ts","sourceRoot":"","sources":["../../src/extension/numeric.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,qEAAqE;AACrE,qBAAa,sBAAuB,SAAQ,KAAK;IAE7C,QAAQ,CAAC,KAAK,EAAE,OAAO;gBAAd,KAAK,EAAE,OAAO,EACvB,KAAK,CAAC,EAAE,MAAM;CASjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAI,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAwB7E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAI,GAAG,MAAM,CAMjE;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,CAAC,EAC1B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,EACzB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAIR;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAU9D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQzD"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Turning what PostgreSQL sends into what JavaScript can add.
3
+ *
4
+ * `BIGINT`, `NUMERIC`, `DECIMAL` and every `COUNT`/`SUM`/`AVG` aggregate arrive
5
+ * from the driver as **strings** — Bun's `SQL` exposes no type-parser hook, so
6
+ * there is no place to fix this once for the whole process. That leaves the call
7
+ * site, and the call site is where it goes wrong:
8
+ *
9
+ * total + row.amount // "0" + "12.50" → "012.50"
10
+ * ("012.50") - 1 // NaN
11
+ *
12
+ * Only `+` misbehaves; `-`, `*`, `/`, `<` and `>` coerce and give correct
13
+ * answers. So the failure hides until a sum happens to enter the chain, and then
14
+ * it does not throw — it produces `NaN`, PostgreSQL accepts `NaN` into a
15
+ * `NUMERIC` column, and `NaN` compares as LARGER than every number. A row poisoned
16
+ * this way passes `WHERE balance > 0`, passes `HAVING total >= required`, and
17
+ * passes a `CHECK (col >= 0)` constraint. The database agrees with the corruption.
18
+ *
19
+ * Hence two rules, both enforced here rather than remembered:
20
+ *
21
+ * * `toNumber` REFUSES `NaN` and `Infinity`. Not "returns 0" — refuses. A value
22
+ * that reached this point non-finite is already the product of a bug, and
23
+ * substituting a plausible number buries it one layer deeper.
24
+ * * `sumNumeric` exists so the common case — adding a column across rows — has
25
+ * an obvious right answer that is shorter to write than the wrong one.
26
+ *
27
+ * This lives in the SDK, not the engine, on purpose. The safe helper existing
28
+ * somewhere an extension cannot import it is a shape this codebase has produced
29
+ * four times already — `safeFetch`, `maybeDecrypt`, `requireInstanceAdmin` and
30
+ * the validation evaluator all had an engine-side implementation and an
31
+ * extension-side reimplementation that was subtly worse. Every module that
32
+ * handles money is an extension, so the conversion belongs where they can reach
33
+ * it: `import { toNumber } from '@zveltio/sdk/extension'`.
34
+ *
35
+ * The engine keeps its own copy at `lib/numeric.ts` rather than importing this
36
+ * one, because the engine's production code has never taken a RUNTIME dependency
37
+ * on the SDK — only types — and a pure sixty-line converter is not the thing to
38
+ * change that for. `numeric-parity.test.ts` runs both against the same table of
39
+ * inputs so the twins cannot drift apart quietly.
40
+ *
41
+ * @see `PgNumeric` in the engine's `db/schema.ts` for the type side of the same problem.
42
+ */
43
+ /** Thrown instead of quietly producing a number nobody can trace. */
44
+ export class NumericConversionError extends Error {
45
+ value;
46
+ constructor(value, label) {
47
+ super(`${label ? `${label}: ` : ''}expected a finite number, got ${typeof value === 'string' ? JSON.stringify(value) : String(value)}`);
48
+ this.value = value;
49
+ this.name = 'NumericConversionError';
50
+ }
51
+ }
52
+ /**
53
+ * A database numeric as a JS number, or a throw.
54
+ *
55
+ * Accepts what the driver actually produces: a string, a number that survived
56
+ * because the column was `INTEGER`, a `bigint`, or `null`/`undefined` for a
57
+ * nullable column. Everything else, and anything non-finite, is an error.
58
+ *
59
+ * `null` and `undefined` map to `fallback`, which defaults to `0`. That is the
60
+ * one substitution worth making: "no row" and "no value" genuinely do mean zero
61
+ * in an aggregate, and the alternative is a `?? 0` at every call site, which is
62
+ * the shape that lets a real `NaN` through.
63
+ */
64
+ export function toNumber(value, fallback = 0, label) {
65
+ if (value === null || value === undefined)
66
+ return fallback;
67
+ if (typeof value === 'bigint') {
68
+ // Above 2^53 a bigint cannot round-trip through a double. Refusing beats
69
+ // returning a number that is quietly off by a few.
70
+ if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
71
+ throw new NumericConversionError(value, label);
72
+ }
73
+ return Number(value);
74
+ }
75
+ if (typeof value === 'number') {
76
+ if (!Number.isFinite(value))
77
+ throw new NumericConversionError(value, label);
78
+ return value;
79
+ }
80
+ if (typeof value === 'string') {
81
+ const trimmed = value.trim();
82
+ // `Number('')` is 0 and `Number(' ')` is 0 — both of which would turn a
83
+ // malformed column into a confident zero.
84
+ if (trimmed === '')
85
+ throw new NumericConversionError(value, label);
86
+ const n = Number(trimmed);
87
+ if (!Number.isFinite(n))
88
+ throw new NumericConversionError(value, label);
89
+ return n;
90
+ }
91
+ throw new NumericConversionError(value, label);
92
+ }
93
+ /**
94
+ * `toNumber` for a value that is allowed to be absent, with no substitution.
95
+ *
96
+ * Use it where zero and "not set" mean different things — a quota that has not
97
+ * been configured is not a quota of zero.
98
+ */
99
+ export function toNumberOrNull(value, label) {
100
+ if (value === null || value === undefined)
101
+ return null;
102
+ return toNumber(value, 0, label);
103
+ }
104
+ /**
105
+ * `toNumber` that answers `fallback` instead of throwing.
106
+ *
107
+ * For display paths only — a dashboard tile should not 500 because one row is
108
+ * bad. Never use it on a value about to be written back to the database, which
109
+ * is exactly how the poison spreads.
110
+ */
111
+ export function toNumberSafe(value, fallback = 0) {
112
+ try {
113
+ return toNumber(value, fallback);
114
+ }
115
+ catch {
116
+ return fallback;
117
+ }
118
+ }
119
+ /** Sum one numeric column across rows, without a `+` on a string in sight. */
120
+ export function sumNumeric(rows, pick, label) {
121
+ let total = 0;
122
+ for (const row of rows)
123
+ total += toNumber(pick(row), 0, label);
124
+ return total;
125
+ }
126
+ /**
127
+ * Round to a currency's minor unit.
128
+ *
129
+ * Money read as a string and converted to a double is exact for every value a
130
+ * `NUMERIC(14,2)` can hold, but arithmetic on doubles is not: `0.1 + 0.2` is
131
+ * `0.30000000000000004`, and written back that is a `NUMERIC` with fourteen
132
+ * decimal places. Round at the point the result becomes an amount again.
133
+ */
134
+ export function roundMoney(value, decimals = 2) {
135
+ if (!Number.isFinite(value))
136
+ throw new NumericConversionError(value, 'roundMoney');
137
+ const factor = 10 ** decimals;
138
+ // Round the MAGNITUDE, then restore the sign. `Math.round` breaks ties
139
+ // towards +Infinity, so a plain `Math.round(v * factor)` rounds 2.675 up to
140
+ // 2.68 and -2.675 up to -2.67 — a credit note and the charge it reverses
141
+ // stop cancelling, and `finance/accounting` computes its balance-sheet
142
+ // difference from exactly that subtraction. Money rounds away from zero.
143
+ const sign = value < 0 ? -1 : 1;
144
+ return (sign * Math.round((Math.abs(value) + Number.EPSILON) * factor)) / factor;
145
+ }
146
+ /**
147
+ * Is this value safe to store in a `NUMERIC` column?
148
+ *
149
+ * The database will not tell you. `INSERT ... VALUES ('NaN'::numeric)` succeeds,
150
+ * and so does a `CHECK (col >= 0)` on the row afterwards. Guard on the way in.
151
+ */
152
+ export function isStorableNumeric(value) {
153
+ if (value === null || value === undefined)
154
+ return true;
155
+ try {
156
+ toNumber(value);
157
+ return true;
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
@@ -27,8 +27,23 @@
27
27
  * Place AFTER the auth-guard middleware that sets `c.set('user', ...)`.
28
28
  */
29
29
  type MaybePromise<T> = T | Promise<T>;
30
+ interface Granter {
31
+ name: string;
32
+ }
33
+ interface DenialContext {
34
+ resource: string;
35
+ action: string;
36
+ confidential: boolean;
37
+ canGrant: Granter[];
38
+ }
30
39
  interface GateContext {
31
40
  checkPermission: (userId: string, resource: string, action: string) => Promise<boolean>;
41
+ /**
42
+ * Supplied by the host so a refusal can say who to ask. Optional because an
43
+ * extension may be running against an engine older than this, and a missing
44
+ * courtesy must never become a missing refusal.
45
+ */
46
+ describeDenial?: (resource: string, action: string) => Promise<DenialContext>;
32
47
  }
33
48
  export declare function permissionGate(ctx: GateContext, resource: string, opts?: {
34
49
  actionOverrides?: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../src/extension/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,UAAU,WAAW;IACnB,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACzF;AAuCD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,IAEzC,GAAG,GAAG,EAAE,MAAM,MAAM,YAAY,CAAC,OAAO,CAAC,kBAgCxD"}
1
+ {"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../src/extension/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,UAAU,OAAO;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAC/E;AAmGD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,IAGzC,GAAG,GAAG,EAAE,MAAM,MAAM,YAAY,CAAC,OAAO,CAAC,kBAgCxD"}
@@ -26,6 +26,36 @@
26
26
  *
27
27
  * Place AFTER the auth-guard middleware that sets `c.set('user', ...)`.
28
28
  */
29
+ async function refuse(ctx, c, resource, action) {
30
+ let denial = null;
31
+ try {
32
+ denial = (await ctx.describeDenial?.(resource, action)) ?? null;
33
+ }
34
+ catch {
35
+ /* the refusal matters more than the suggestion */
36
+ }
37
+ const names = denial?.canGrant.map((g) => g.name) ?? [];
38
+ const who = names.length === 0
39
+ ? 'An administrator of this workspace'
40
+ : names.length === 1
41
+ ? names[0]
42
+ : `${names.slice(0, -1).join(', ')} or ${names[names.length - 1]}`;
43
+ const what = denial?.confidential
44
+ ? `${resource} is confidential`
45
+ : `you do not have access to ${resource}`;
46
+ const detail = names.length === 0 ? `${what}. ${who} can grant it.` : `${what}. ${who} can give you access.`;
47
+ return c.json({
48
+ type: 'about:blank',
49
+ title: 'Forbidden',
50
+ status: 403,
51
+ code: 'permission_required',
52
+ detail,
53
+ resource,
54
+ action,
55
+ confidential: denial?.confidential ?? false,
56
+ can_grant: names.map((name) => ({ name })),
57
+ }, 403);
58
+ }
29
59
  function methodToAction(method) {
30
60
  const m = method.toUpperCase();
31
61
  switch (m) {
@@ -56,10 +86,12 @@ function methodToAction(method) {
56
86
  * Strict (default) is the secure posture and what the SDK ships with.
57
87
  */
58
88
  function rbacMode() {
89
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
59
90
  const v = globalThis.process?.env?.EXTENSION_RBAC;
60
91
  return v === 'permissive' ? 'permissive' : 'strict';
61
92
  }
62
93
  export function permissionGate(ctx, resource, opts = {}) {
94
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
63
95
  return async (c, next) => {
64
96
  // CORS preflight (OPTIONS) must never be authorization-gated —
65
97
  // browsers send it without credentials and a 401/403 here breaks
@@ -86,7 +118,7 @@ export function permissionGate(ctx, resource, opts = {}) {
86
118
  await next();
87
119
  return;
88
120
  }
89
- return c.json({ error: `Forbidden: missing ${resource}:${action} permission` }, 403);
121
+ return refuse(ctx, c, resource, action);
90
122
  }
91
123
  await next();
92
124
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { ZveltioClient, createZveltioClient } from './client.js';
2
2
  export type { ZveltioClientConfig, ListParams, ListResult } from './client.js';
3
+ export { ZveltioApiError, PROBLEM_CONTENT_TYPE } from './errors.js';
4
+ export type { ProblemDetails } from './errors.js';
3
5
  export { ZveltioRealtime } from './realtime.js';
4
6
  export { LocalStore } from './local-store.js';
5
7
  export { SyncManager } from './sync-manager.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAG1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAG1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ZveltioClient, createZveltioClient } from './client.js';
2
+ export { ZveltioApiError, PROBLEM_CONTENT_TYPE } from './errors.js';
2
3
  export { ZveltioRealtime } from './realtime.js';
3
4
  export { LocalStore } from './local-store.js';
4
5
  export { SyncManager } from './sync-manager.js';
@@ -51,6 +51,11 @@ export declare class LocalStore {
51
51
  markFailed(queueItemId: string, error: string): Promise<void>;
52
52
  /** Apply data from server (via WebSocket or pull) */
53
53
  applyServerUpdate(collection: string, id: string, data: Record<string, any>, serverVersion: number): Promise<void>;
54
+ /**
55
+ * The server deleted a record: drop the local copy WITHOUT queueing a delete.
56
+ * `delete()` would queue one, echoing the server's own deletion back at it.
57
+ */
58
+ applyServerDelete(collection: string, id: string): Promise<void>;
54
59
  /** Get records with conflicts for UI resolution */
55
60
  getConflicts(collection?: string): Promise<LocalRecord[]>;
56
61
  /** Resolve a conflict (user decides which version wins) */
@@ -1 +1 @@
1
- {"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["../src/local-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["../src/local-store.ts"],"names":[],"mappings":"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"}