silosdk 0.0.0 → 0.0.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 (85) hide show
  1. package/README.md +3 -1
  2. package/dist/_virtual/rolldown_runtime.cjs +29 -0
  3. package/dist/cli/d1.cjs +93 -0
  4. package/dist/cli/d1.mjs +92 -0
  5. package/dist/cli/index.cjs +93 -0
  6. package/dist/cli/index.d.cts +1 -0
  7. package/dist/cli/index.d.mts +1 -0
  8. package/dist/cli/index.mjs +94 -0
  9. package/dist/cli/init.cjs +134 -0
  10. package/dist/cli/init.mjs +133 -0
  11. package/dist/cli/kv.cjs +63 -0
  12. package/dist/cli/kv.mjs +60 -0
  13. package/dist/cli/r2.cjs +83 -0
  14. package/dist/cli/r2.mjs +82 -0
  15. package/dist/cli/wrangler.cjs +93 -0
  16. package/dist/cli/wrangler.mjs +89 -0
  17. package/dist/local/adapters/cloudflare.cjs +200 -0
  18. package/dist/local/adapters/cloudflare.d.cts +50 -0
  19. package/dist/local/adapters/cloudflare.d.mts +50 -0
  20. package/dist/local/adapters/cloudflare.mjs +200 -0
  21. package/dist/local/auth-context.cjs +14 -0
  22. package/dist/local/auth-context.d.cts +7 -0
  23. package/dist/local/auth-context.d.mts +7 -0
  24. package/dist/local/auth-context.mjs +12 -0
  25. package/dist/local/auth.cjs +109 -0
  26. package/dist/local/auth.d.cts +26 -0
  27. package/dist/local/auth.d.mts +26 -0
  28. package/dist/local/auth.mjs +99 -0
  29. package/dist/local/commit.cjs +350 -0
  30. package/dist/local/commit.d.cts +59 -0
  31. package/dist/local/commit.d.mts +59 -0
  32. package/dist/local/commit.mjs +349 -0
  33. package/dist/local/config.cjs +17 -0
  34. package/dist/local/config.mjs +15 -0
  35. package/dist/local/index.cjs +16 -0
  36. package/dist/local/index.d.cts +10 -0
  37. package/dist/local/index.d.mts +10 -0
  38. package/dist/local/index.mjs +9 -0
  39. package/dist/local/provider.cjs +204 -0
  40. package/dist/local/provider.d.cts +25 -0
  41. package/dist/local/provider.d.mts +25 -0
  42. package/dist/local/provider.mjs +203 -0
  43. package/dist/local/query-store.cjs +276 -0
  44. package/dist/local/query-store.mjs +274 -0
  45. package/dist/local/storage.cjs +71 -0
  46. package/dist/local/storage.d.cts +7 -0
  47. package/dist/local/storage.d.mts +7 -0
  48. package/dist/local/storage.mjs +68 -0
  49. package/dist/local/sync.cjs +124 -0
  50. package/dist/local/sync.d.cts +36 -0
  51. package/dist/local/sync.d.mts +36 -0
  52. package/dist/local/sync.mjs +122 -0
  53. package/dist/local/view.cjs +257 -0
  54. package/dist/local/view.d.cts +24 -0
  55. package/dist/local/view.d.mts +24 -0
  56. package/dist/local/view.mjs +254 -0
  57. package/dist/package.cjs +11 -0
  58. package/dist/package.mjs +5 -0
  59. package/dist/schema/index.cjs +276 -0
  60. package/dist/schema/index.d.cts +207 -0
  61. package/dist/schema/index.d.mts +207 -0
  62. package/dist/schema/index.mjs +265 -0
  63. package/dist/server/auth.cjs +132 -0
  64. package/dist/server/auth.d.cts +49 -0
  65. package/dist/server/auth.d.mts +49 -0
  66. package/dist/server/auth.mjs +122 -0
  67. package/dist/server/d1.cjs +120 -0
  68. package/dist/server/d1.mjs +116 -0
  69. package/dist/server/do.cjs +132 -0
  70. package/dist/server/do.d.cts +21 -0
  71. package/dist/server/do.d.mts +21 -0
  72. package/dist/server/do.mjs +131 -0
  73. package/dist/server/index.cjs +355 -0
  74. package/dist/server/index.d.cts +65 -0
  75. package/dist/server/index.d.mts +65 -0
  76. package/dist/server/index.mjs +348 -0
  77. package/dist/server/protect.cjs +34 -0
  78. package/dist/server/protect.d.cts +32 -0
  79. package/dist/server/protect.d.mts +32 -0
  80. package/dist/server/protect.mjs +33 -0
  81. package/dist/server/r2.cjs +58 -0
  82. package/dist/server/r2.d.cts +4 -0
  83. package/dist/server/r2.d.mts +4 -0
  84. package/dist/server/r2.mjs +53 -0
  85. package/package.json +55 -2
@@ -0,0 +1,348 @@
1
+ import { view } from "../schema/index.mjs";
2
+ import { SyncObject } from "./do.mjs";
3
+ import { d1ApplyOps, d1EnsureSchema, d1GetRow, d1GetRows } from "./d1.mjs";
4
+ import { isAssetOwner, parseAssetKeyScope, r2Delete, r2Get, r2Put } from "./r2.mjs";
5
+ import { createAnonymousUser, createAuth, createOtp, createSession, customProvider, deleteSession, findOrCreateUser, otpProvider, resolveSession, verifyOtp } from "./auth.mjs";
6
+ import { protect } from "./protect.mjs";
7
+
8
+ //#region src/server/index.ts
9
+ async function d1EnsureAuthSchema(db) {
10
+ await db.batch([db.prepare(`CREATE TABLE IF NOT EXISTS silo_users (
11
+ id TEXT PRIMARY KEY,
12
+ email TEXT UNIQUE,
13
+ role TEXT NOT NULL DEFAULT 'user',
14
+ createdAt TEXT NOT NULL
15
+ )`), db.prepare(`CREATE TABLE IF NOT EXISTS silo_otps (
16
+ email TEXT PRIMARY KEY,
17
+ code TEXT NOT NULL,
18
+ expiresAt TEXT NOT NULL
19
+ )`)]);
20
+ }
21
+ function createServer(config) {
22
+ const { auth, views, storage } = config;
23
+ const storageConfig = storage == null ? null : storage === true ? {} : storage || null;
24
+ const storageMaxSize = storageConfig?.maxSize ?? 50 * 1024 * 1024;
25
+ const protectedViews = /* @__PURE__ */ new Map();
26
+ for (const v of views) if ("_rules" in v) protectedViews.set(v.name, v);
27
+ let schemaReady = false;
28
+ async function ensureSchema(db) {
29
+ if (schemaReady) return;
30
+ await d1EnsureSchema(db);
31
+ if (auth && Array.isArray(auth._providers)) await d1EnsureAuthSchema(db);
32
+ schemaReady = true;
33
+ }
34
+ function checkRolePermission(viewName, operation, role) {
35
+ if (!auth?._roles) return true;
36
+ const roleDef = auth._roles[role];
37
+ if (!roleDef) return false;
38
+ return (roleDef[viewName] ?? roleDef.default)[operation];
39
+ }
40
+ async function resolveUser(request, env) {
41
+ const url = new URL(request.url);
42
+ const authHeader = request.headers.get("Authorization");
43
+ const token = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null) ?? url.searchParams.get("token");
44
+ if (auth) {
45
+ if (!token) return null;
46
+ if (!Array.isArray(auth._providers) && auth._providers._type === "custom") {
47
+ const resolved = await auth._providers.resolve(token);
48
+ if (!resolved) return null;
49
+ return {
50
+ id: resolved.id,
51
+ email: null,
52
+ role: resolved.role ?? "user",
53
+ isAnonymous: false
54
+ };
55
+ }
56
+ const user = await resolveSession(env.SILO_SESSIONS, token);
57
+ if (user && url.pathname === "/sync" && user.isAnonymous) return null;
58
+ return user;
59
+ }
60
+ const devUser = request.headers.get("X-Dev-User");
61
+ return devUser ? {
62
+ id: devUser,
63
+ email: null,
64
+ role: "user",
65
+ isAnonymous: false
66
+ } : {
67
+ id: "dev",
68
+ email: null,
69
+ role: "user",
70
+ isAnonymous: false
71
+ };
72
+ }
73
+ return {
74
+ SyncObject,
75
+ async fetch(request, env) {
76
+ await ensureSchema(env.SILO_DB);
77
+ const url = new URL(request.url);
78
+ const { pathname } = url;
79
+ if (pathname === "/health") return json({ ok: true });
80
+ if (auth && Array.isArray(auth._providers) && pathname.startsWith("/auth/")) return handleAuthRoute(pathname, request, env, auth);
81
+ if (storageConfig && pathname === "/storage/upload" && request.method === "POST") {
82
+ const user = await resolveUser(request, env);
83
+ if (!user || user.isAnonymous) return new Response("Unauthorized", { status: 401 });
84
+ const scope = request.headers.get("X-Asset-Scope") === "public" ? "public" : "private";
85
+ const contentLengthHeader = request.headers.get("Content-Length");
86
+ const declaredSize = contentLengthHeader ? Number(contentLengthHeader) : 0;
87
+ if (Number.isFinite(declaredSize) && declaredSize > storageMaxSize) return new Response("Payload too large", { status: 413 });
88
+ if (!request.body) return new Response("Missing request body", { status: 400 });
89
+ const contentType = request.headers.get("Content-Type") ?? "application/octet-stream";
90
+ const name = request.headers.get("X-File-Name") ?? void 0;
91
+ const uploaded = await r2Put(env.SILO_STORAGE, {
92
+ scope,
93
+ userId: user.id,
94
+ body: request.body,
95
+ name,
96
+ contentType,
97
+ size: Number.isFinite(declaredSize) && declaredSize > 0 ? declaredSize : void 0
98
+ });
99
+ if (storageConfig.onUpload) {
100
+ if (!await storageConfig.onUpload({
101
+ user,
102
+ key: uploaded.key,
103
+ contentType: uploaded.contentType ?? contentType,
104
+ size: uploaded.size ?? 0,
105
+ scope,
106
+ env
107
+ })) {
108
+ await r2Delete(env.SILO_STORAGE, uploaded.key);
109
+ return new Response("Forbidden", { status: 403 });
110
+ }
111
+ }
112
+ return json(uploaded);
113
+ }
114
+ const storageMatch = pathname.match(/^\/storage\/(.+)$/);
115
+ if (storageConfig && storageMatch && request.method === "GET") {
116
+ const key = decodeURIComponent(storageMatch[1]);
117
+ const parsed = parseAssetKeyScope(key);
118
+ if (!parsed) return new Response("Not found", { status: 404 });
119
+ if (parsed.scope === "private") {
120
+ const user = await resolveUser(request, env);
121
+ if (!user) return new Response("Unauthorized", { status: 401 });
122
+ if (!isAssetOwner(key, user.id)) return new Response("Forbidden", { status: 403 });
123
+ } else if (!storageConfig.public) {
124
+ if (!await resolveUser(request, env)) return new Response("Unauthorized", { status: 401 });
125
+ }
126
+ const obj = await r2Get(env.SILO_STORAGE, key);
127
+ if (!obj || !obj.body) return new Response("Not found", { status: 404 });
128
+ const headers = new Headers();
129
+ if (obj.httpMetadata?.contentType) headers.set("Content-Type", obj.httpMetadata.contentType);
130
+ else headers.set("Content-Type", "application/octet-stream");
131
+ return new Response(obj.body, {
132
+ status: 200,
133
+ headers
134
+ });
135
+ }
136
+ if (storageConfig && storageMatch && request.method === "DELETE") {
137
+ const user = await resolveUser(request, env);
138
+ if (!user) return new Response("Unauthorized", { status: 401 });
139
+ const key = decodeURIComponent(storageMatch[1]);
140
+ if (!parseAssetKeyScope(key)) return new Response("Not found", { status: 404 });
141
+ if (!isAssetOwner(key, user.id)) return new Response("Forbidden", { status: 403 });
142
+ await r2Delete(env.SILO_STORAGE, key);
143
+ return new Response(null, { status: 204 });
144
+ }
145
+ const publicMatch = pathname.match(/^\/public\/([^/]+)$/);
146
+ if (publicMatch) {
147
+ const viewName = decodeURIComponent(publicMatch[1]);
148
+ const protected_ = protectedViews.get(viewName);
149
+ const hasReadRules = protected_?._rules.read != null;
150
+ const hasWriteRules = protected_?._rules.write != null;
151
+ if (request.method === "GET") {
152
+ const whereParam = url.searchParams.get("where");
153
+ const orderParam = url.searchParams.get("order");
154
+ let where;
155
+ let order;
156
+ if (whereParam) try {
157
+ const parsed = JSON.parse(whereParam);
158
+ if (parsed && typeof parsed === "object") where = parsed;
159
+ } catch {
160
+ return new Response("Invalid where clause", { status: 400 });
161
+ }
162
+ if (orderParam) try {
163
+ const parsed = JSON.parse(orderParam);
164
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) order = parsed;
165
+ } catch {
166
+ return new Response("Invalid order clause", { status: 400 });
167
+ }
168
+ if (!hasReadRules && !checkRolePermission(viewName, "read", "anon")) {
169
+ const id$1 = url.searchParams.get("id");
170
+ if (id$1) {
171
+ const row = await d1GetRow(env.SILO_DB, viewName, { id: id$1 });
172
+ if (!row) return new Response("Not found", { status: 404 });
173
+ return json(row);
174
+ } else {
175
+ const take = url.searchParams.get("take");
176
+ const options = {};
177
+ if (take) options.take = Number(take);
178
+ if (where) options.where = where;
179
+ if (order) options.order = order;
180
+ const pv = url.searchParams.get("parentView");
181
+ const pi = url.searchParams.get("parentId");
182
+ if (pv && pi) {
183
+ options.parentView = pv;
184
+ options.parentId = pi;
185
+ }
186
+ const cv = url.searchParams.get("childView");
187
+ const ci = url.searchParams.get("childId");
188
+ if (cv && ci) {
189
+ options.childView = cv;
190
+ options.childId = ci;
191
+ }
192
+ return json(await d1GetRows(env.SILO_DB, viewName, options));
193
+ }
194
+ }
195
+ const user = await resolveUser(request, env);
196
+ if (!user && (hasReadRules || hasWriteRules)) return new Response("Unauthorized", { status: 401 });
197
+ if (hasReadRules) {
198
+ const query = where && !Array.isArray(where) ? { ...where } : {};
199
+ if (!await protected_._rules.read({
200
+ user,
201
+ env,
202
+ query
203
+ })) return new Response("Forbidden", { status: 403 });
204
+ } else if (!checkRolePermission(viewName, "read", user?.role ?? "anon")) return new Response("Forbidden", { status: 403 });
205
+ const id = url.searchParams.get("id");
206
+ if (id) {
207
+ const row = await d1GetRow(env.SILO_DB, viewName, { id });
208
+ if (!row) return new Response("Not found", { status: 404 });
209
+ return json(row);
210
+ } else {
211
+ const take = url.searchParams.get("take");
212
+ const options = {};
213
+ if (take) options.take = Number(take);
214
+ if (where) options.where = where;
215
+ if (order) options.order = order;
216
+ const pv = url.searchParams.get("parentView");
217
+ const pi = url.searchParams.get("parentId");
218
+ if (pv && pi) {
219
+ options.parentView = pv;
220
+ options.parentId = pi;
221
+ }
222
+ const cv = url.searchParams.get("childView");
223
+ const ci = url.searchParams.get("childId");
224
+ if (cv && ci) {
225
+ options.childView = cv;
226
+ options.childId = ci;
227
+ }
228
+ return json(await d1GetRows(env.SILO_DB, viewName, options));
229
+ }
230
+ }
231
+ if (request.method === "POST") {
232
+ const user = await resolveUser(request, env);
233
+ if (!user) return new Response("Unauthorized", { status: 401 });
234
+ let body;
235
+ try {
236
+ body = await request.json();
237
+ } catch {
238
+ return new Response("Invalid JSON", { status: 400 });
239
+ }
240
+ if (!Array.isArray(body?.ops)) return new Response("Expected { ops: [...] }", { status: 400 });
241
+ if (user.isAnonymous && !protected_?._rules.write) return json({
242
+ error: "Anonymous users cannot write to public DB",
243
+ code: "ANON_WRITE_FORBIDDEN"
244
+ }, 403);
245
+ if (protected_?._rules.write) {
246
+ for (const op of body.ops) if (!await protected_._rules.write({
247
+ user,
248
+ env,
249
+ op
250
+ })) return new Response("Forbidden", { status: 403 });
251
+ } else if (!checkRolePermission(viewName, "write", user.role)) return new Response("Forbidden", { status: 403 });
252
+ await d1ApplyOps(env.SILO_DB, body.ops);
253
+ return new Response(null, { status: 204 });
254
+ }
255
+ return new Response("Method not allowed", { status: 405 });
256
+ }
257
+ if (pathname === "/sync" && request.headers.get("Upgrade") === "websocket") {
258
+ const user = await resolveUser(request, env);
259
+ if (!user) return new Response("Unauthorized", { status: 401 });
260
+ const doId = env.SILO_SYNC.idFromName(user.id);
261
+ return env.SILO_SYNC.get(doId).fetch(request);
262
+ }
263
+ return new Response("Not found", { status: 404 });
264
+ }
265
+ };
266
+ async function handleAuthRoute(pathname, request, env, authInst) {
267
+ const sessionDuration = authInst._sessionDuration;
268
+ const otpProv = authInst._providers.find((p) => p._type === "otp");
269
+ if (pathname === "/auth/anonymous" && request.method === "POST") {
270
+ const { token, userId } = await createAnonymousUser(env.SILO_DB, env.SILO_SESSIONS, sessionDuration, authInst._defaultRole);
271
+ return json({
272
+ token,
273
+ user: {
274
+ id: userId,
275
+ email: null,
276
+ role: authInst._defaultRole,
277
+ isAnonymous: true
278
+ }
279
+ });
280
+ }
281
+ if (pathname === "/auth/request" && request.method === "POST") {
282
+ if (!otpProv) return new Response("OTP auth not enabled", { status: 400 });
283
+ let body;
284
+ try {
285
+ body = await request.json();
286
+ } catch {
287
+ return new Response("Invalid JSON", { status: 400 });
288
+ }
289
+ if (!body.email) return new Response("Missing email", { status: 400 });
290
+ const code = await createOtp(env.SILO_DB, body.email);
291
+ await otpProv.config.sendOTP(body.email, code, env);
292
+ return json({ ok: true });
293
+ }
294
+ if (pathname === "/auth/verify" && request.method === "POST") {
295
+ let body;
296
+ try {
297
+ body = await request.json();
298
+ } catch {
299
+ return new Response("Invalid JSON", { status: 400 });
300
+ }
301
+ if (!body.email || !body.otp) return new Response("Missing email or otp", { status: 400 });
302
+ const { valid } = await verifyOtp(env.SILO_DB, body.email, body.otp);
303
+ if (!valid) return new Response("Invalid or expired code", { status: 401 });
304
+ const user = await findOrCreateUser(env.SILO_DB, body.email, { role: authInst._defaultRole });
305
+ return json({
306
+ token: await createSession(env.SILO_SESSIONS, user.id, user.email, user.role, sessionDuration),
307
+ user: {
308
+ id: user.id,
309
+ email: user.email,
310
+ role: user.role,
311
+ isAnonymous: false
312
+ }
313
+ });
314
+ }
315
+ if (pathname === "/auth/signout" && request.method === "POST") {
316
+ const authHeader = request.headers.get("Authorization");
317
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
318
+ if (token) await deleteSession(env.SILO_SESSIONS, token);
319
+ return json({ ok: true });
320
+ }
321
+ if (pathname === "/auth/me" && request.method === "GET") {
322
+ const user = await resolveSessionFromRequest(request, env);
323
+ if (!user) return new Response("Unauthorized", { status: 401 });
324
+ return json({
325
+ id: user.id,
326
+ email: user.email,
327
+ role: user.role,
328
+ isAnonymous: user.isAnonymous
329
+ });
330
+ }
331
+ return new Response("Not found", { status: 404 });
332
+ }
333
+ async function resolveSessionFromRequest(request, env) {
334
+ const authHeader = request.headers.get("Authorization");
335
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
336
+ if (!token) return null;
337
+ return resolveSession(env.SILO_SESSIONS, token);
338
+ }
339
+ }
340
+ function json(data, status = 200) {
341
+ return new Response(JSON.stringify(data), {
342
+ status,
343
+ headers: { "Content-Type": "application/json" }
344
+ });
345
+ }
346
+
347
+ //#endregion
348
+ export { SyncObject, createAuth, createServer, customProvider, otpProvider, protect, resolveSession, view };
@@ -0,0 +1,34 @@
1
+
2
+ //#region src/server/protect.ts
3
+ /**
4
+ * Wrap a view with access-control rules for the public DB.
5
+ *
6
+ * When a rule is defined, it runs INSTEAD of any role-based permission check
7
+ * (roles configured via `createAuth`). The rule receives `user.role` in context
8
+ * and can incorporate it if desired.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Rule replaces role check entirely
13
+ * const ProtectedTicketRequest = protect(TicketRequest, {
14
+ * write: ({ user, op }) => {
15
+ * // Runners/admins can write any request
16
+ * if (user.role === 'runner' || user.role === 'admin') return true
17
+ * // Users can only create requests for themselves
18
+ * return (op.value as any)?.userId === user.id
19
+ * },
20
+ * })
21
+ *
22
+ * createServer({ views: [ProtectedTicketRequest], auth })
23
+ * ```
24
+ */
25
+ function protect(view, rules) {
26
+ return {
27
+ name: view.name,
28
+ _protected: true,
29
+ _rules: rules
30
+ };
31
+ }
32
+
33
+ //#endregion
34
+ exports.protect = protect;
@@ -0,0 +1,32 @@
1
+ import { ProtectedView, ReadRuleContext, View, WriteRuleContext } from "../schema/index.cjs";
2
+
3
+ //#region src/server/protect.d.ts
4
+
5
+ /**
6
+ * Wrap a view with access-control rules for the public DB.
7
+ *
8
+ * When a rule is defined, it runs INSTEAD of any role-based permission check
9
+ * (roles configured via `createAuth`). The rule receives `user.role` in context
10
+ * and can incorporate it if desired.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Rule replaces role check entirely
15
+ * const ProtectedTicketRequest = protect(TicketRequest, {
16
+ * write: ({ user, op }) => {
17
+ * // Runners/admins can write any request
18
+ * if (user.role === 'runner' || user.role === 'admin') return true
19
+ * // Users can only create requests for themselves
20
+ * return (op.value as any)?.userId === user.id
21
+ * },
22
+ * })
23
+ *
24
+ * createServer({ views: [ProtectedTicketRequest], auth })
25
+ * ```
26
+ */
27
+ declare function protect<V extends View>(view: V, rules: {
28
+ read?: (ctx: ReadRuleContext) => boolean | Promise<boolean>;
29
+ write?: (ctx: WriteRuleContext) => boolean | Promise<boolean>;
30
+ }): ProtectedView<V['name']>;
31
+ //#endregion
32
+ export { protect };
@@ -0,0 +1,32 @@
1
+ import { ProtectedView, ReadRuleContext, View, WriteRuleContext } from "../schema/index.mjs";
2
+
3
+ //#region src/server/protect.d.ts
4
+
5
+ /**
6
+ * Wrap a view with access-control rules for the public DB.
7
+ *
8
+ * When a rule is defined, it runs INSTEAD of any role-based permission check
9
+ * (roles configured via `createAuth`). The rule receives `user.role` in context
10
+ * and can incorporate it if desired.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Rule replaces role check entirely
15
+ * const ProtectedTicketRequest = protect(TicketRequest, {
16
+ * write: ({ user, op }) => {
17
+ * // Runners/admins can write any request
18
+ * if (user.role === 'runner' || user.role === 'admin') return true
19
+ * // Users can only create requests for themselves
20
+ * return (op.value as any)?.userId === user.id
21
+ * },
22
+ * })
23
+ *
24
+ * createServer({ views: [ProtectedTicketRequest], auth })
25
+ * ```
26
+ */
27
+ declare function protect<V extends View>(view: V, rules: {
28
+ read?: (ctx: ReadRuleContext) => boolean | Promise<boolean>;
29
+ write?: (ctx: WriteRuleContext) => boolean | Promise<boolean>;
30
+ }): ProtectedView<V['name']>;
31
+ //#endregion
32
+ export { protect };
@@ -0,0 +1,33 @@
1
+ //#region src/server/protect.ts
2
+ /**
3
+ * Wrap a view with access-control rules for the public DB.
4
+ *
5
+ * When a rule is defined, it runs INSTEAD of any role-based permission check
6
+ * (roles configured via `createAuth`). The rule receives `user.role` in context
7
+ * and can incorporate it if desired.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // Rule replaces role check entirely
12
+ * const ProtectedTicketRequest = protect(TicketRequest, {
13
+ * write: ({ user, op }) => {
14
+ * // Runners/admins can write any request
15
+ * if (user.role === 'runner' || user.role === 'admin') return true
16
+ * // Users can only create requests for themselves
17
+ * return (op.value as any)?.userId === user.id
18
+ * },
19
+ * })
20
+ *
21
+ * createServer({ views: [ProtectedTicketRequest], auth })
22
+ * ```
23
+ */
24
+ function protect(view, rules) {
25
+ return {
26
+ name: view.name,
27
+ _protected: true,
28
+ _rules: rules
29
+ };
30
+ }
31
+
32
+ //#endregion
33
+ export { protect };
@@ -0,0 +1,58 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ let nanoid = require("nanoid");
3
+
4
+ //#region src/server/r2.ts
5
+ const DEFAULT_CONTENT_TYPE = "application/octet-stream";
6
+ function createAssetKey(scope, userId) {
7
+ return `${scope}/${userId}/${(0, nanoid.nanoid)()}`;
8
+ }
9
+ function parseAssetKeyScope(key) {
10
+ const parts = key.split("/");
11
+ if (parts.length < 3) return null;
12
+ const [scopeRaw, ownerId, ...rest] = parts;
13
+ if (scopeRaw !== "private" && scopeRaw !== "public") return null;
14
+ if (!ownerId) return null;
15
+ const objectId = rest.join("/");
16
+ if (!objectId) return null;
17
+ return {
18
+ scope: scopeRaw,
19
+ ownerId,
20
+ objectId
21
+ };
22
+ }
23
+ function isAssetOwner(key, userId) {
24
+ const parsed = parseAssetKeyScope(key);
25
+ if (!parsed) return false;
26
+ return parsed.ownerId === userId;
27
+ }
28
+ async function r2Put(bucket, input) {
29
+ const key = createAssetKey(input.scope, input.userId);
30
+ await bucket.put(key, input.body, {
31
+ httpMetadata: { contentType: input.contentType ?? DEFAULT_CONTENT_TYPE },
32
+ customMetadata: {
33
+ scope: input.scope,
34
+ ownerId: input.userId,
35
+ ...input.name ? { name: input.name } : {},
36
+ ...typeof input.size === "number" ? { size: String(input.size) } : {}
37
+ }
38
+ });
39
+ return {
40
+ key,
41
+ ...input.name ? { name: input.name } : {},
42
+ ...input.contentType ? { contentType: input.contentType } : {},
43
+ ...typeof input.size === "number" ? { size: input.size } : {}
44
+ };
45
+ }
46
+ async function r2Get(bucket, key) {
47
+ return bucket.get(key);
48
+ }
49
+ async function r2Delete(bucket, key) {
50
+ await bucket.delete(key);
51
+ }
52
+
53
+ //#endregion
54
+ exports.isAssetOwner = isAssetOwner;
55
+ exports.parseAssetKeyScope = parseAssetKeyScope;
56
+ exports.r2Delete = r2Delete;
57
+ exports.r2Get = r2Get;
58
+ exports.r2Put = r2Put;
@@ -0,0 +1,4 @@
1
+ //#region src/server/r2.d.ts
2
+ type AssetScope = 'private' | 'public';
3
+ //#endregion
4
+ export { AssetScope };
@@ -0,0 +1,4 @@
1
+ //#region src/server/r2.d.ts
2
+ type AssetScope = 'private' | 'public';
3
+ //#endregion
4
+ export { AssetScope };
@@ -0,0 +1,53 @@
1
+ import { nanoid } from "nanoid";
2
+
3
+ //#region src/server/r2.ts
4
+ const DEFAULT_CONTENT_TYPE = "application/octet-stream";
5
+ function createAssetKey(scope, userId) {
6
+ return `${scope}/${userId}/${nanoid()}`;
7
+ }
8
+ function parseAssetKeyScope(key) {
9
+ const parts = key.split("/");
10
+ if (parts.length < 3) return null;
11
+ const [scopeRaw, ownerId, ...rest] = parts;
12
+ if (scopeRaw !== "private" && scopeRaw !== "public") return null;
13
+ if (!ownerId) return null;
14
+ const objectId = rest.join("/");
15
+ if (!objectId) return null;
16
+ return {
17
+ scope: scopeRaw,
18
+ ownerId,
19
+ objectId
20
+ };
21
+ }
22
+ function isAssetOwner(key, userId) {
23
+ const parsed = parseAssetKeyScope(key);
24
+ if (!parsed) return false;
25
+ return parsed.ownerId === userId;
26
+ }
27
+ async function r2Put(bucket, input) {
28
+ const key = createAssetKey(input.scope, input.userId);
29
+ await bucket.put(key, input.body, {
30
+ httpMetadata: { contentType: input.contentType ?? DEFAULT_CONTENT_TYPE },
31
+ customMetadata: {
32
+ scope: input.scope,
33
+ ownerId: input.userId,
34
+ ...input.name ? { name: input.name } : {},
35
+ ...typeof input.size === "number" ? { size: String(input.size) } : {}
36
+ }
37
+ });
38
+ return {
39
+ key,
40
+ ...input.name ? { name: input.name } : {},
41
+ ...input.contentType ? { contentType: input.contentType } : {},
42
+ ...typeof input.size === "number" ? { size: input.size } : {}
43
+ };
44
+ }
45
+ async function r2Get(bucket, key) {
46
+ return bucket.get(key);
47
+ }
48
+ async function r2Delete(bucket, key) {
49
+ await bucket.delete(key);
50
+ }
51
+
52
+ //#endregion
53
+ export { isAssetOwner, parseAssetKeyScope, r2Delete, r2Get, r2Put };
package/package.json CHANGED
@@ -1,5 +1,58 @@
1
1
  {
2
2
  "name": "silosdk",
3
- "version": "0.0.0",
4
- "packageManager": "yarn@4.9.2"
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "tsdown",
7
+ "watch": "tsdown --watch",
8
+ "publish": "tsdown && npm publish"
9
+ },
10
+ "packageManager": "yarn@4.9.2",
11
+ "files": [
12
+ "dist/"
13
+ ],
14
+ "bin": {
15
+ "silo": "./dist/cli/index.mjs"
16
+ },
17
+ "devDependencies": {
18
+ "@cloudflare/workers-types": "^4.20260219.0",
19
+ "@types/node": "^25.3.0",
20
+ "@types/react": "^19",
21
+ "expo-secure-store": "*",
22
+ "expo-sqlite": "*",
23
+ "prettier": "^3.7.4",
24
+ "tsdown": "^0.17.2",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "dependencies": {
28
+ "@standard-schema/spec": "^1.0.0",
29
+ "jotai": "^2.15.1",
30
+ "jotai-family": "^1.0.1",
31
+ "nanoid": "5.1.6"
32
+ },
33
+ "peerDependencies": {
34
+ "expo-secure-store": "*",
35
+ "expo-sqlite": "*",
36
+ "react": "^19"
37
+ },
38
+ "prettier": {
39
+ "semi": false,
40
+ "singleQuote": true,
41
+ "trailingComma": "all",
42
+ "printWidth": 80,
43
+ "tabWidth": 2
44
+ },
45
+ "exports": {
46
+ "./schema": {
47
+ "types": "./dist/schema/index.d.mts",
48
+ "import": "./dist/schema/index.mjs",
49
+ "require": "./dist/schema/index.cjs"
50
+ },
51
+ "./local": "./dist/local/index.cjs",
52
+ "./server": {
53
+ "types": "./dist/server/index.d.mts",
54
+ "import": "./dist/server/index.mjs"
55
+ },
56
+ "./package.json": "./package.json"
57
+ }
5
58
  }