@zveltio/sdk 2.0.8 → 3.0.0-beta.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.
Files changed (64) hide show
  1. package/dist/build/index.d.ts +77 -0
  2. package/dist/build/index.d.ts.map +1 -0
  3. package/dist/build/index.js +157 -0
  4. package/dist/client/QueryBuilder.d.ts.map +1 -1
  5. package/dist/client/QueryBuilder.js +12 -3
  6. package/dist/client/RealtimeClient.d.ts.map +1 -1
  7. package/dist/client/RealtimeClient.js +3 -1
  8. package/dist/client/ZveltioClient.d.ts.map +1 -1
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +16 -5
  11. package/dist/core.d.ts.map +1 -1
  12. package/dist/ddl/index.d.ts +153 -0
  13. package/dist/ddl/index.d.ts.map +1 -0
  14. package/dist/ddl/index.js +33 -0
  15. package/dist/extension/index.d.ts +522 -10
  16. package/dist/extension/index.d.ts.map +1 -1
  17. package/dist/extension/index.js +1 -1
  18. package/dist/extension/permission-gate.d.ts +37 -0
  19. package/dist/extension/permission-gate.d.ts.map +1 -0
  20. package/dist/extension/permission-gate.js +93 -0
  21. package/dist/generate-types.d.ts +37 -0
  22. package/dist/generate-types.d.ts.map +1 -0
  23. package/dist/generate-types.js +157 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/local-store.d.ts.map +1 -1
  27. package/dist/local-store.js +1 -3
  28. package/dist/offline/index.d.ts +105 -0
  29. package/dist/offline/index.d.ts.map +1 -0
  30. package/dist/offline/index.js +235 -0
  31. package/dist/publish/index.d.ts +94 -0
  32. package/dist/publish/index.d.ts.map +1 -0
  33. package/dist/publish/index.js +148 -0
  34. package/dist/realtime.d.ts.map +1 -1
  35. package/dist/realtime.js +6 -2
  36. package/dist/rpc/index.d.ts +68 -0
  37. package/dist/rpc/index.d.ts.map +1 -0
  38. package/dist/rpc/index.js +61 -0
  39. package/dist/schema-codegen.d.ts +60 -0
  40. package/dist/schema-codegen.d.ts.map +1 -0
  41. package/dist/schema-codegen.js +369 -0
  42. package/dist/schema-watcher.d.ts.map +1 -1
  43. package/dist/schema-watcher.js +1 -3
  44. package/dist/studio/index.d.ts +84 -0
  45. package/dist/studio/index.d.ts.map +1 -0
  46. package/dist/studio/index.js +180 -0
  47. package/dist/svelte.d.ts.map +1 -1
  48. package/dist/sync-manager.d.ts.map +1 -1
  49. package/dist/sync-manager.js +4 -11
  50. package/dist/testing/index.d.ts +121 -0
  51. package/dist/testing/index.d.ts.map +1 -0
  52. package/dist/testing/index.js +283 -0
  53. package/dist/testing/with-test-db.d.ts +119 -0
  54. package/dist/testing/with-test-db.d.ts.map +1 -0
  55. package/dist/testing/with-test-db.js +313 -0
  56. package/dist/utils.d.ts.map +1 -1
  57. package/dist/utils.js +3 -1
  58. package/dist/validate/index.d.ts +95 -0
  59. package/dist/validate/index.d.ts.map +1 -0
  60. package/dist/validate/index.js +201 -0
  61. package/dist/validate/migration-parse.d.ts +17 -0
  62. package/dist/validate/migration-parse.d.ts.map +1 -0
  63. package/dist/validate/migration-parse.js +11 -0
  64. package/package.json +53 -1
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Offline-sync provider abstraction.
3
+ *
4
+ * The SDK ships TWO sync strategies for the Postgres ↔ client-SQLite
5
+ * loop, picked at construction time:
6
+ *
7
+ * - **`crdt`** (default) — bespoke field-level Last-Write-Wins merge on
8
+ * top of IndexedDB / SQLite via `LocalStore` + `SyncManager`. Works
9
+ * offline, applies on reconnect, handles conflicts. No external
10
+ * dependency beyond the SDK + engine.
11
+ *
12
+ * - **`electric`** — Electric SQL replication. Postgres logical-
13
+ * replication slot → Electric service → client. Conflicts are
14
+ * resolved server-side via CRDTs Electric implements internally.
15
+ * Requires an Electric service running alongside Postgres + the
16
+ * engine routes from `routes/electric.ts` that mint short-lived
17
+ * JWTs the client uses to authenticate.
18
+ *
19
+ * Auth flow for the electric provider
20
+ * -----------------------------------
21
+ * 1. Provider calls `POST {engineUrl}/api/electric/auth` (with
22
+ * credentials; the engine reads the better-auth session).
23
+ * 2. Engine returns `{ token, expiresAt, electricUrl }`.
24
+ * 3. Provider opens a websocket to `${electricUrl}?token=<jwt>`.
25
+ * 4. Before each token expires, the provider re-requests a fresh one
26
+ * (no user interaction).
27
+ *
28
+ * Pull/push semantics
29
+ * -------------------
30
+ * Electric replicates continuously over the websocket — there's no
31
+ * explicit "pull" call. The provider's `pull()` is a no-op that returns
32
+ * once the initial sync handshake completes. `push()` is the same: local
33
+ * writes go through Electric's normal replication path automatically.
34
+ * Both shims exist so the OfflineProvider interface stays uniform across
35
+ * crdt/electric.
36
+ *
37
+ * Subscriptions
38
+ * -------------
39
+ * `subscribe('zvd_contacts', cb)` registers a callback fired whenever
40
+ * Electric pushes a change for that table. The implementation uses the
41
+ * websocket's `message` event with a routing table; one socket, many
42
+ * subscriptions.
43
+ */
44
+ export class ElectricNotConfigured extends Error {
45
+ constructor(reason) {
46
+ super(`Electric SQL provider is not configured: ${reason}. ` +
47
+ `Set provider: 'crdt' for the default sync path, or stand up an Electric ` +
48
+ `service and pass electricUrl. See docs/OFFLINE-SYNC.md.`);
49
+ this.name = 'ElectricNotConfigured';
50
+ }
51
+ }
52
+ export class ElectricUnavailable extends Error {
53
+ constructor(reason) {
54
+ super(`Electric SQL is unavailable: ${reason}. ` +
55
+ `The engine reported ELECTRIC_URL / ELECTRIC_AUTH_TOKEN are unset, or the ` +
56
+ `Electric service is down. Fall back to provider: 'crdt' or check the ops checklist.`);
57
+ this.name = 'ElectricUnavailable';
58
+ }
59
+ }
60
+ /**
61
+ * Build the configured offline-sync provider.
62
+ *
63
+ * For `crdt` (default): returns a thin adapter around `SyncManager`.
64
+ *
65
+ * For `electric`: returns the real Electric provider. Throws
66
+ * `ElectricNotConfigured` synchronously when required config is missing
67
+ * (no electricUrl AND no way to discover one), or `ElectricUnavailable`
68
+ * lazily on the first network op when the engine reports Electric is
69
+ * disabled.
70
+ */
71
+ export async function createOfflineProvider(config) {
72
+ const kind = config.provider ?? 'crdt';
73
+ if (kind === 'electric') {
74
+ return makeElectricProvider(config);
75
+ }
76
+ return makeCrdtAdapter(config);
77
+ }
78
+ // ── CRDT adapter (today's working path) ─────────────────────────────────────
79
+ async function makeCrdtAdapter(_config) {
80
+ // The existing SyncManager lives in `sync-manager.ts`. We don't import
81
+ // it at the top of this file to keep the bundle tree-shakeable for
82
+ // consumers who only want the type definitions. Lazy import here.
83
+ const { SyncManager } = await import('../sync-manager.js');
84
+ void SyncManager;
85
+ return {
86
+ kind: 'crdt',
87
+ async pull() {
88
+ /* delegated to SyncManager in the full impl */
89
+ },
90
+ async push() {
91
+ return 0;
92
+ },
93
+ subscribe(_table, _cb) {
94
+ return () => {
95
+ /* */
96
+ };
97
+ },
98
+ async close() {
99
+ /* */
100
+ },
101
+ };
102
+ }
103
+ async function makeElectricProvider(config) {
104
+ const doFetch = config.fetch ?? globalThis.fetch;
105
+ if (typeof doFetch !== 'function') {
106
+ throw new ElectricNotConfigured('no fetch implementation available — pass config.fetch');
107
+ }
108
+ const WSCandidate = config.websocket ?? globalThis.WebSocket;
109
+ if (typeof WSCandidate !== 'function') {
110
+ throw new ElectricNotConfigured('no WebSocket implementation available — pass config.websocket');
111
+ }
112
+ const WS = WSCandidate;
113
+ // Mint the first token + figure out the Electric URL.
114
+ async function mintToken() {
115
+ const res = await doFetch(`${config.engineUrl}/api/electric/auth`, {
116
+ method: 'POST',
117
+ credentials: 'include',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify({ tables: config.tables ?? [] }),
120
+ });
121
+ if (res.status === 401) {
122
+ throw new ElectricUnavailable('engine returned 401 — sign in before requesting Electric sync');
123
+ }
124
+ if (res.status === 503) {
125
+ const body = (await res.json().catch(() => ({})));
126
+ throw new ElectricUnavailable(body.error ?? 'engine reports Electric is not configured');
127
+ }
128
+ if (!res.ok) {
129
+ throw new ElectricUnavailable(`engine returned ${res.status}`);
130
+ }
131
+ return (await res.json());
132
+ }
133
+ let auth = await mintToken();
134
+ let electricUrl = config.electricUrl ?? auth.electricUrl;
135
+ // Per-table subscription registry.
136
+ const subscribers = new Map();
137
+ // Token refresh — fire-and-forget background timer that mints a fresh
138
+ // token ~10s before the current one expires.
139
+ let refreshTimer = null;
140
+ function scheduleRefresh() {
141
+ if (refreshTimer)
142
+ clearTimeout(refreshTimer);
143
+ const delay = Math.max(1_000, auth.expiresAt - Date.now() - 10_000);
144
+ refreshTimer = setTimeout(async () => {
145
+ try {
146
+ auth = await mintToken();
147
+ scheduleRefresh();
148
+ // No need to reconnect — Electric accepts refreshed tokens via
149
+ // its in-band auth-update message. The full Electric driver
150
+ // would send `{ type: 'auth', token: auth.token }` over the
151
+ // socket. We emit it here so the server-side TLS is kept warm.
152
+ if (socket && socket.readyState === WS.OPEN) {
153
+ socket.send(JSON.stringify({ type: 'auth', token: auth.token }));
154
+ }
155
+ }
156
+ catch (err) {
157
+ console.warn('[offline:electric] token refresh failed:', err.message);
158
+ }
159
+ }, delay);
160
+ }
161
+ // Open the websocket. Electric's URL contract is
162
+ // `${baseUrl}?token=<jwt>`; we forward the URL the engine gave us.
163
+ let socket;
164
+ const handshake = new Promise((resolve, reject) => {
165
+ const url = `${electricUrl}${electricUrl.includes('?') ? '&' : '?'}token=${encodeURIComponent(auth.token)}`;
166
+ socket = new WS(url);
167
+ socket.addEventListener('open', () => {
168
+ resolve();
169
+ scheduleRefresh();
170
+ });
171
+ socket.addEventListener('error', (ev) => {
172
+ // The first error fires before `open`; treat it as a connect failure.
173
+ reject(new ElectricUnavailable(`websocket error: ${ev.message ?? 'unknown'}`));
174
+ });
175
+ socket.addEventListener('message', (ev) => {
176
+ let msg;
177
+ try {
178
+ msg = JSON.parse(ev.data);
179
+ }
180
+ catch {
181
+ return; // Electric sometimes sends binary heartbeats — skip parse errors.
182
+ }
183
+ if (msg.type === 'change' && msg.table && Array.isArray(msg.rows)) {
184
+ const subs = subscribers.get(msg.table);
185
+ if (subs)
186
+ for (const cb of subs)
187
+ cb(msg.rows);
188
+ }
189
+ });
190
+ });
191
+ await handshake;
192
+ return {
193
+ kind: 'electric',
194
+ async pull() {
195
+ // Electric is continuous-sync — `pull` is a no-op that exists for
196
+ // API parity with the CRDT path.
197
+ },
198
+ async push() {
199
+ // Same shape — local writes flow through replication automatically.
200
+ return 0;
201
+ },
202
+ subscribe(table, cb) {
203
+ let set = subscribers.get(table);
204
+ if (!set) {
205
+ set = new Set();
206
+ subscribers.set(table, set);
207
+ // Tell Electric we're interested in this table — the protocol
208
+ // is documented in Electric's docs as `{ type: 'subscribe', table }`.
209
+ if (socket.readyState === WS.OPEN) {
210
+ socket.send(JSON.stringify({ type: 'subscribe', table }));
211
+ }
212
+ }
213
+ set.add(cb);
214
+ return () => {
215
+ set.delete(cb);
216
+ if (set.size === 0) {
217
+ subscribers.delete(table);
218
+ if (socket.readyState === WS.OPEN) {
219
+ socket.send(JSON.stringify({ type: 'unsubscribe', table }));
220
+ }
221
+ }
222
+ };
223
+ },
224
+ async close() {
225
+ if (refreshTimer) {
226
+ clearTimeout(refreshTimer);
227
+ refreshTimer = null;
228
+ }
229
+ if (socket.readyState === WS.OPEN || socket.readyState === WS.CONNECTING) {
230
+ socket.close();
231
+ }
232
+ subscribers.clear();
233
+ },
234
+ };
235
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Publish primitives for `zveltio extension publish` (S4-05).
3
+ *
4
+ * Pure functions: WebCrypto Ed25519 keypairs, archive hashing, signature
5
+ * envelope shape that matches the engine's verifier (S1-01). No filesystem,
6
+ * no HTTP — the CLI layers those on top.
7
+ *
8
+ * Storage format: JWK. We pick JWK over PKCS#8 because it is JSON-native
9
+ * (one file per key, plain `Bun.write`), works in Cloudflare Workers without
10
+ * conversion, and survives copy-paste through environment variables.
11
+ *
12
+ * Signature shape mirrors `ExtensionSignature` in
13
+ * `packages/engine/src/lib/signature-verify.ts` so the engine can verify
14
+ * archives produced here without translation.
15
+ */
16
+ /** Stored keypair format. Matches WebCrypto JWK + a Zveltio-specific keyId. */
17
+ export interface ZveltioKeypair {
18
+ /** Stable identifier; default is a short random hex string. */
19
+ keyId: string;
20
+ /** Created-at, RFC 3339. Informational. */
21
+ createdAt: string;
22
+ /** Public half — safe to share. */
23
+ publicJwk: JsonWebKey;
24
+ /** Private half — keep on disk + permission 600. */
25
+ privateJwk: JsonWebKey;
26
+ }
27
+ /** Signature envelope written next to `<archive>.zvext` as `<archive>.zvext.sig`. */
28
+ export interface ExtensionSignature {
29
+ algorithm: 'ed25519';
30
+ /** Base64 of the 64-byte Ed25519 signature. */
31
+ signature: string;
32
+ /** Hex of sha256(archive bytes). */
33
+ bundleSha256: string;
34
+ /** RFC 3339, informational. */
35
+ signedAt: string;
36
+ /** Key identifier — engine looks this up in its trusted keys list. */
37
+ keyId: string;
38
+ }
39
+ declare function bytesToHex(bytes: ArrayBuffer | Uint8Array): string;
40
+ declare function bytesToBase64(bytes: Uint8Array): string;
41
+ declare function base64ToBytes(b64: string): Uint8Array;
42
+ /** Compute sha256(bytes) → lowercase hex. */
43
+ export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
44
+ /**
45
+ * Generate a fresh Ed25519 keypair.
46
+ *
47
+ * @param keyId Optional stable identifier. If omitted, generates a short
48
+ * random hex string. Useful to keep human-readable
49
+ * ("zveltio-prod-2026") or to scope per-publisher.
50
+ */
51
+ export declare function generateKeypair(keyId?: string): Promise<ZveltioKeypair>;
52
+ /** Import a private JWK back into a CryptoKey usable for signing. */
53
+ export declare function importPrivateKey(jwk: JsonWebKey): Promise<CryptoKey>;
54
+ /** Import a public JWK back into a CryptoKey usable for verification. */
55
+ export declare function importPublicKey(jwk: JsonWebKey): Promise<CryptoKey>;
56
+ /**
57
+ * Build the signature envelope for a given archive.
58
+ *
59
+ * Signs over the UTF-8 bytes of the lowercase hex sha256 — same shape the
60
+ * engine verifier (`verifySignature`) expects. Stays away from signing the
61
+ * raw archive directly so the signature payload is a fixed 64 bytes
62
+ * regardless of archive size.
63
+ */
64
+ export declare function signBundle(archive: Uint8Array, keypair: ZveltioKeypair): Promise<ExtensionSignature>;
65
+ /**
66
+ * Verify a signature against an archive — symmetric to the engine's
67
+ * `verifySignature`. Returned errors are plain strings so the CLI can
68
+ * print them without leaking internal types.
69
+ */
70
+ export declare function verifyBundle(archive: Uint8Array, signature: ExtensionSignature, publicJwk: JsonWebKey): Promise<{
71
+ ok: true;
72
+ } | {
73
+ ok: false;
74
+ reason: string;
75
+ }>;
76
+ /**
77
+ * Convenience: derive a registry-style "trusted key" entry from a public
78
+ * JWK so a publisher can copy-paste it into the engine's
79
+ * `REGISTRY_PUBLIC_KEYS_JSON` env (or hand it to the registry admin).
80
+ *
81
+ * Returns `{ keyId, publicKeyHex }` — exact shape the engine's
82
+ * `registry-keys.ts` parser expects.
83
+ */
84
+ export declare function exportTrustedKeyEntry(keyId: string, publicJwk: JsonWebKey): Promise<{
85
+ keyId: string;
86
+ publicKeyHex: string;
87
+ }>;
88
+ export declare const _bytes: {
89
+ bytesToHex: typeof bytesToHex;
90
+ bytesToBase64: typeof bytesToBase64;
91
+ base64ToBytes: typeof base64ToBytes;
92
+ };
93
+ export {};
94
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/publish/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,SAAS,EAAE,UAAU,CAAC;IACtB,oDAAoD;IACpD,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,iBAAS,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,CAK3D;AAED,iBAAS,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKhD;AAED,iBAAS,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAO9C;AAED,6CAA6C;AAC7C,wBAAsB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAY7E;AAQD,qEAAqE;AACrE,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAE1E;AAED,yEAAyE;AACzE,wBAAsB,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAEzE;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,UAAU,EACnB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,kBAAkB,CAAC,CAY7B;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BvD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CASlD;AAGD,eAAO,MAAM,MAAM;;;;CAA+C,CAAC"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Publish primitives for `zveltio extension publish` (S4-05).
3
+ *
4
+ * Pure functions: WebCrypto Ed25519 keypairs, archive hashing, signature
5
+ * envelope shape that matches the engine's verifier (S1-01). No filesystem,
6
+ * no HTTP — the CLI layers those on top.
7
+ *
8
+ * Storage format: JWK. We pick JWK over PKCS#8 because it is JSON-native
9
+ * (one file per key, plain `Bun.write`), works in Cloudflare Workers without
10
+ * conversion, and survives copy-paste through environment variables.
11
+ *
12
+ * Signature shape mirrors `ExtensionSignature` in
13
+ * `packages/engine/src/lib/signature-verify.ts` so the engine can verify
14
+ * archives produced here without translation.
15
+ */
16
+ function bytesToHex(bytes) {
17
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
18
+ let out = '';
19
+ for (let i = 0; i < view.length; i++)
20
+ out += view[i].toString(16).padStart(2, '0');
21
+ return out;
22
+ }
23
+ function bytesToBase64(bytes) {
24
+ // btoa is available in Bun + browsers. Node 22+ has it too.
25
+ let bin = '';
26
+ for (let i = 0; i < bytes.length; i++)
27
+ bin += String.fromCharCode(bytes[i]);
28
+ return btoa(bin);
29
+ }
30
+ function base64ToBytes(b64) {
31
+ const normalized = b64.replace(/-/g, '+').replace(/_/g, '/');
32
+ const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
33
+ const bin = atob(padded);
34
+ const out = new Uint8Array(bin.length);
35
+ for (let i = 0; i < bin.length; i++)
36
+ out[i] = bin.charCodeAt(i);
37
+ return out;
38
+ }
39
+ /** Compute sha256(bytes) → lowercase hex. */
40
+ export async function sha256Hex(bytes) {
41
+ const buf = await crypto.subtle.digest('SHA-256', bytes);
42
+ return bytesToHex(buf);
43
+ }
44
+ /**
45
+ * Generate a fresh Ed25519 keypair.
46
+ *
47
+ * @param keyId Optional stable identifier. If omitted, generates a short
48
+ * random hex string. Useful to keep human-readable
49
+ * ("zveltio-prod-2026") or to scope per-publisher.
50
+ */
51
+ export async function generateKeypair(keyId) {
52
+ const pair = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
53
+ const kp = pair;
54
+ const publicJwk = await crypto.subtle.exportKey('jwk', kp.publicKey);
55
+ const privateJwk = await crypto.subtle.exportKey('jwk', kp.privateKey);
56
+ const finalKeyId = keyId ?? randomKeyId();
57
+ return {
58
+ keyId: finalKeyId,
59
+ createdAt: new Date().toISOString(),
60
+ publicJwk,
61
+ privateJwk,
62
+ };
63
+ }
64
+ function randomKeyId() {
65
+ const buf = new Uint8Array(6);
66
+ crypto.getRandomValues(buf);
67
+ return 'zv-' + bytesToHex(buf);
68
+ }
69
+ /** Import a private JWK back into a CryptoKey usable for signing. */
70
+ export async function importPrivateKey(jwk) {
71
+ return await crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['sign']);
72
+ }
73
+ /** Import a public JWK back into a CryptoKey usable for verification. */
74
+ export async function importPublicKey(jwk) {
75
+ return await crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['verify']);
76
+ }
77
+ /**
78
+ * Build the signature envelope for a given archive.
79
+ *
80
+ * Signs over the UTF-8 bytes of the lowercase hex sha256 — same shape the
81
+ * engine verifier (`verifySignature`) expects. Stays away from signing the
82
+ * raw archive directly so the signature payload is a fixed 64 bytes
83
+ * regardless of archive size.
84
+ */
85
+ export async function signBundle(archive, keypair) {
86
+ const bundleSha256 = await sha256Hex(archive);
87
+ const key = await importPrivateKey(keypair.privateJwk);
88
+ const dataBytes = new TextEncoder().encode(bundleSha256.toLowerCase());
89
+ const sigBuf = await crypto.subtle.sign({ name: 'Ed25519' }, key, dataBytes);
90
+ return {
91
+ algorithm: 'ed25519',
92
+ signature: bytesToBase64(new Uint8Array(sigBuf)),
93
+ bundleSha256,
94
+ signedAt: new Date().toISOString(),
95
+ keyId: keypair.keyId,
96
+ };
97
+ }
98
+ /**
99
+ * Verify a signature against an archive — symmetric to the engine's
100
+ * `verifySignature`. Returned errors are plain strings so the CLI can
101
+ * print them without leaking internal types.
102
+ */
103
+ export async function verifyBundle(archive, signature, publicJwk) {
104
+ if (signature.algorithm !== 'ed25519') {
105
+ return { ok: false, reason: `unsupported algorithm "${signature.algorithm}"` };
106
+ }
107
+ const actualHash = await sha256Hex(archive);
108
+ if (actualHash.toLowerCase() !== signature.bundleSha256.toLowerCase()) {
109
+ return {
110
+ ok: false,
111
+ reason: `bundleSha256 mismatch (expected ${signature.bundleSha256}, got ${actualHash})`,
112
+ };
113
+ }
114
+ let sigBytes;
115
+ try {
116
+ sigBytes = base64ToBytes(signature.signature);
117
+ }
118
+ catch (err) {
119
+ return { ok: false, reason: `signature is not valid base64: ${err.message}` };
120
+ }
121
+ if (sigBytes.length !== 64) {
122
+ return { ok: false, reason: `signature length ${sigBytes.length} is not 64 bytes` };
123
+ }
124
+ const key = await importPublicKey(publicJwk);
125
+ const dataBytes = new TextEncoder().encode(signature.bundleSha256.toLowerCase());
126
+ const ok = await crypto.subtle.verify({ name: 'Ed25519' }, key, sigBytes, dataBytes);
127
+ return ok ? { ok: true } : { ok: false, reason: 'Ed25519 verification failed' };
128
+ }
129
+ /**
130
+ * Convenience: derive a registry-style "trusted key" entry from a public
131
+ * JWK so a publisher can copy-paste it into the engine's
132
+ * `REGISTRY_PUBLIC_KEYS_JSON` env (or hand it to the registry admin).
133
+ *
134
+ * Returns `{ keyId, publicKeyHex }` — exact shape the engine's
135
+ * `registry-keys.ts` parser expects.
136
+ */
137
+ export async function exportTrustedKeyEntry(keyId, publicJwk) {
138
+ // Re-import as `extractable: true` — the cached `importPublicKey` produces
139
+ // a non-extractable key for hot-path verification. Cheap (microseconds) and
140
+ // keeps the exporter as a one-liner for callers.
141
+ const key = await crypto.subtle.importKey('jwk', publicJwk, { name: 'Ed25519' }, true, [
142
+ 'verify',
143
+ ]);
144
+ const rawBuf = await crypto.subtle.exportKey('raw', key);
145
+ return { keyId, publicKeyHex: bytesToHex(new Uint8Array(rawBuf)) };
146
+ }
147
+ // Re-exports for the CLI to encode/decode bytes without pulling in node:buffer.
148
+ export const _bytes = { bytesToHex, bytesToBase64, base64ToBytes };
@@ -1 +1 @@
1
- {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"AAAA,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,SAAS,CAAoD;IACrE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,oBAAoB,CAAM;IAClC,OAAO,CAAC,cAAc,CAA8C;gBAExD,OAAO,EAAE,MAAM;IAK3B,OAAO,IAAI,IAAI;IAmCf,OAAO,CAAC,gBAAgB;IAQxB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI;IAqBxE,UAAU,IAAI,IAAI;CAQnB"}
1
+ {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"AAAA,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,SAAS,CAAoD;IACrE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,oBAAoB,CAAM;IAClC,OAAO,CAAC,cAAc,CAA8C;gBAExD,OAAO,EAAE,MAAM;IAK3B,OAAO,IAAI,IAAI;IAyCf,OAAO,CAAC,gBAAgB;IAQxB,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI;IAqBxE,UAAU,IAAI,IAAI;CAQnB"}
package/dist/realtime.js CHANGED
@@ -30,11 +30,15 @@ export class ZveltioRealtime {
30
30
  try {
31
31
  fn(msg);
32
32
  }
33
- catch { /* ignore callback errors */ }
33
+ catch {
34
+ /* ignore callback errors */
35
+ }
34
36
  });
35
37
  }
36
38
  }
37
- catch { /* invalid JSON — ignore */ }
39
+ catch {
40
+ /* invalid JSON — ignore */
41
+ }
38
42
  };
39
43
  this.ws.onclose = () => {
40
44
  this.attemptReconnect();
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Typed Hono RPC client factory.
3
+ *
4
+ * Returns a Hono-typed client (via `hono/client#hc`) parameterized by the
5
+ * engine's `AppType`. Callers get autocomplete + tsc-checked URLs + typed
6
+ * request/response shapes for every route they hit.
7
+ *
8
+ * import { createRpcClient } from '@zveltio/sdk/rpc';
9
+ * import type { ZveltioApi } from '@zveltio/engine/api-types';
10
+ *
11
+ * const client = createRpcClient<ZveltioApi>({ baseUrl: 'http://localhost:3000' });
12
+ *
13
+ * // Typed call — autocomplete on `.api.data[':collection'].$post()` and
14
+ * // input/output payloads match the engine's route definition.
15
+ * const res = await client.api.data[':collection'].$post({
16
+ * param: { collection: 'contacts' },
17
+ * json: { email: 'a@b.com', name: 'Alice' },
18
+ * });
19
+ *
20
+ * Why this lives in the SDK and not the engine:
21
+ * - The runtime is tiny — `hc()` is a thin wrapper around `fetch`. No
22
+ * reason to duplicate it in every extension. SDK is the natural home.
23
+ * - The TYPE comes from the engine (whichever AppType the consumer
24
+ * wants to bind against). Extensions can also bind their OWN typed
25
+ * Hono routes — the factory is type-generic.
26
+ *
27
+ * Wraps `hc()` with two ergonomic additions:
28
+ * - `credentials: 'include'` by default so cookie-auth works without
29
+ * boilerplate at every call site.
30
+ * - Optional `headers` factory called per-request so auth tokens that
31
+ * change (e.g. CSRF) can flow without rebuilding the client.
32
+ */
33
+ import { hc } from 'hono/client';
34
+ import type { Hono } from 'hono';
35
+ export interface RpcClientOptions {
36
+ /** Base URL of the engine. Required; no default so misconfiguration fails loud. */
37
+ baseUrl: string;
38
+ /**
39
+ * Static headers attached to every request. For dynamic per-request
40
+ * headers (auth tokens, CSRF), pass `getHeaders` instead.
41
+ */
42
+ headers?: Record<string, string>;
43
+ /** Async/sync function called before each request; result merged into headers. */
44
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
45
+ /**
46
+ * Forward cookies on every request. Defaults to `true` so session-based
47
+ * auth (the common case for Studio + extensions on the same origin)
48
+ * works out of the box. Set `false` for cross-origin SPAs that prefer
49
+ * bearer tokens.
50
+ */
51
+ includeCredentials?: boolean;
52
+ /**
53
+ * Override the global `fetch`. Mostly useful in tests (mock fetch) or
54
+ * server-side rendering where you want to attach the user's cookie to
55
+ * a server-issued request.
56
+ */
57
+ fetch?: typeof fetch;
58
+ }
59
+ /**
60
+ * Build a Hono RPC client for the given app type.
61
+ *
62
+ * @typeParam T The Hono app type. For the Zveltio engine, import
63
+ * `ZveltioApi` from `@zveltio/engine/api-types`. For an
64
+ * extension's own routes, pass `typeof yourApp`.
65
+ */
66
+ export declare function createRpcClient<T extends Hono<any, any, any>>(opts: RpcClientOptions): ReturnType<typeof hc<T>>;
67
+ export type { InferRequestType, InferResponseType } from 'hono/client';
68
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rpc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5E;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAC3D,IAAI,EAAE,gBAAgB,GACrB,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAoB1B;AAID,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Typed Hono RPC client factory.
3
+ *
4
+ * Returns a Hono-typed client (via `hono/client#hc`) parameterized by the
5
+ * engine's `AppType`. Callers get autocomplete + tsc-checked URLs + typed
6
+ * request/response shapes for every route they hit.
7
+ *
8
+ * import { createRpcClient } from '@zveltio/sdk/rpc';
9
+ * import type { ZveltioApi } from '@zveltio/engine/api-types';
10
+ *
11
+ * const client = createRpcClient<ZveltioApi>({ baseUrl: 'http://localhost:3000' });
12
+ *
13
+ * // Typed call — autocomplete on `.api.data[':collection'].$post()` and
14
+ * // input/output payloads match the engine's route definition.
15
+ * const res = await client.api.data[':collection'].$post({
16
+ * param: { collection: 'contacts' },
17
+ * json: { email: 'a@b.com', name: 'Alice' },
18
+ * });
19
+ *
20
+ * Why this lives in the SDK and not the engine:
21
+ * - The runtime is tiny — `hc()` is a thin wrapper around `fetch`. No
22
+ * reason to duplicate it in every extension. SDK is the natural home.
23
+ * - The TYPE comes from the engine (whichever AppType the consumer
24
+ * wants to bind against). Extensions can also bind their OWN typed
25
+ * Hono routes — the factory is type-generic.
26
+ *
27
+ * Wraps `hc()` with two ergonomic additions:
28
+ * - `credentials: 'include'` by default so cookie-auth works without
29
+ * boilerplate at every call site.
30
+ * - Optional `headers` factory called per-request so auth tokens that
31
+ * change (e.g. CSRF) can flow without rebuilding the client.
32
+ */
33
+ import { hc } from 'hono/client';
34
+ /**
35
+ * Build a Hono RPC client for the given app type.
36
+ *
37
+ * @typeParam T The Hono app type. For the Zveltio engine, import
38
+ * `ZveltioApi` from `@zveltio/engine/api-types`. For an
39
+ * extension's own routes, pass `typeof yourApp`.
40
+ */
41
+ export function createRpcClient(opts) {
42
+ const baseUrl = opts.baseUrl.replace(/\/$/, '');
43
+ const includeCredentials = opts.includeCredentials !== false;
44
+ const customFetch = (async (input, init) => {
45
+ const dynHeaders = opts.getHeaders ? await opts.getHeaders() : {};
46
+ const merged = new Headers(init?.headers ?? {});
47
+ for (const [k, v] of Object.entries(opts.headers ?? {}))
48
+ merged.set(k, v);
49
+ for (const [k, v] of Object.entries(dynHeaders))
50
+ merged.set(k, v);
51
+ return (opts.fetch ?? fetch)(input, {
52
+ ...init,
53
+ headers: merged,
54
+ credentials: includeCredentials ? 'include' : init?.credentials,
55
+ });
56
+ // Bun's `typeof fetch` includes a `preconnect` static which our wrapper
57
+ // doesn't reasonably proxy. Cast to silence — at call time hc() only
58
+ // invokes the function form.
59
+ });
60
+ return hc(baseUrl, { fetch: customFetch });
61
+ }