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.
- package/README.md +3 -1
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/cli/d1.cjs +93 -0
- package/dist/cli/d1.mjs +92 -0
- package/dist/cli/index.cjs +93 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +94 -0
- package/dist/cli/init.cjs +134 -0
- package/dist/cli/init.mjs +133 -0
- package/dist/cli/kv.cjs +63 -0
- package/dist/cli/kv.mjs +60 -0
- package/dist/cli/r2.cjs +83 -0
- package/dist/cli/r2.mjs +82 -0
- package/dist/cli/wrangler.cjs +93 -0
- package/dist/cli/wrangler.mjs +89 -0
- package/dist/local/adapters/cloudflare.cjs +200 -0
- package/dist/local/adapters/cloudflare.d.cts +50 -0
- package/dist/local/adapters/cloudflare.d.mts +50 -0
- package/dist/local/adapters/cloudflare.mjs +200 -0
- package/dist/local/auth-context.cjs +14 -0
- package/dist/local/auth-context.d.cts +7 -0
- package/dist/local/auth-context.d.mts +7 -0
- package/dist/local/auth-context.mjs +12 -0
- package/dist/local/auth.cjs +109 -0
- package/dist/local/auth.d.cts +26 -0
- package/dist/local/auth.d.mts +26 -0
- package/dist/local/auth.mjs +99 -0
- package/dist/local/commit.cjs +350 -0
- package/dist/local/commit.d.cts +59 -0
- package/dist/local/commit.d.mts +59 -0
- package/dist/local/commit.mjs +349 -0
- package/dist/local/config.cjs +17 -0
- package/dist/local/config.mjs +15 -0
- package/dist/local/index.cjs +16 -0
- package/dist/local/index.d.cts +10 -0
- package/dist/local/index.d.mts +10 -0
- package/dist/local/index.mjs +9 -0
- package/dist/local/provider.cjs +204 -0
- package/dist/local/provider.d.cts +25 -0
- package/dist/local/provider.d.mts +25 -0
- package/dist/local/provider.mjs +203 -0
- package/dist/local/query-store.cjs +276 -0
- package/dist/local/query-store.mjs +274 -0
- package/dist/local/storage.cjs +71 -0
- package/dist/local/storage.d.cts +7 -0
- package/dist/local/storage.d.mts +7 -0
- package/dist/local/storage.mjs +68 -0
- package/dist/local/sync.cjs +124 -0
- package/dist/local/sync.d.cts +36 -0
- package/dist/local/sync.d.mts +36 -0
- package/dist/local/sync.mjs +122 -0
- package/dist/local/view.cjs +257 -0
- package/dist/local/view.d.cts +24 -0
- package/dist/local/view.d.mts +24 -0
- package/dist/local/view.mjs +254 -0
- package/dist/package.cjs +11 -0
- package/dist/package.mjs +5 -0
- package/dist/schema/index.cjs +276 -0
- package/dist/schema/index.d.cts +207 -0
- package/dist/schema/index.d.mts +207 -0
- package/dist/schema/index.mjs +265 -0
- package/dist/server/auth.cjs +132 -0
- package/dist/server/auth.d.cts +49 -0
- package/dist/server/auth.d.mts +49 -0
- package/dist/server/auth.mjs +122 -0
- package/dist/server/d1.cjs +120 -0
- package/dist/server/d1.mjs +116 -0
- package/dist/server/do.cjs +132 -0
- package/dist/server/do.d.cts +21 -0
- package/dist/server/do.d.mts +21 -0
- package/dist/server/do.mjs +131 -0
- package/dist/server/index.cjs +355 -0
- package/dist/server/index.d.cts +65 -0
- package/dist/server/index.d.mts +65 -0
- package/dist/server/index.mjs +348 -0
- package/dist/server/protect.cjs +34 -0
- package/dist/server/protect.d.cts +32 -0
- package/dist/server/protect.d.mts +32 -0
- package/dist/server/protect.mjs +33 -0
- package/dist/server/r2.cjs +58 -0
- package/dist/server/r2.d.cts +4 -0
- package/dist/server/r2.d.mts +4 -0
- package/dist/server/r2.mjs +53 -0
- 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,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.
|
|
4
|
-
"
|
|
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
|
}
|