deepspace 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +258 -120
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +39 -15
- package/dist/index.js +76 -45
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +72 -73
- package/dist/server.js +368 -337
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +72 -73
- package/dist/worker.js +422 -391
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -165,6 +165,11 @@ var BUILT_IN_TOOLS = [
|
|
|
165
165
|
|
|
166
166
|
// src/server/utils/scoped-r2-files.ts
|
|
167
167
|
var CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
|
|
168
|
+
function contentDisposition(originalName) {
|
|
169
|
+
const safe = originalName.replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
|
|
170
|
+
const encoded = encodeURIComponent(originalName);
|
|
171
|
+
return `inline; filename="${safe}"; filename*=UTF-8''${encoded}`;
|
|
172
|
+
}
|
|
168
173
|
function sanitizeSubpath(raw) {
|
|
169
174
|
if (!raw) return "";
|
|
170
175
|
const segments = raw.split("/");
|
|
@@ -352,339 +357,11 @@ async function handleDownload(bucket, key) {
|
|
|
352
357
|
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
353
358
|
headers.set("Access-Control-Allow-Origin", "*");
|
|
354
359
|
if (object.customMetadata?.originalName) {
|
|
355
|
-
headers.set("Content-Disposition",
|
|
360
|
+
headers.set("Content-Disposition", contentDisposition(object.customMetadata.originalName));
|
|
356
361
|
}
|
|
357
362
|
return new Response(object.body, { headers });
|
|
358
363
|
}
|
|
359
364
|
|
|
360
|
-
// src/server/auth/jwtVerifier.ts
|
|
361
|
-
import { importSPKI, jwtVerify } from "jose";
|
|
362
|
-
|
|
363
|
-
// src/server/auth/utils.ts
|
|
364
|
-
var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
|
|
365
|
-
function bufferToHex(bytes) {
|
|
366
|
-
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
367
|
-
let hex = "";
|
|
368
|
-
for (const byte of view) {
|
|
369
|
-
hex += byte.toString(16).padStart(2, "0");
|
|
370
|
-
}
|
|
371
|
-
return hex;
|
|
372
|
-
}
|
|
373
|
-
function normalizeArray(value) {
|
|
374
|
-
if (value == null) return void 0;
|
|
375
|
-
if (Array.isArray(value)) {
|
|
376
|
-
return value.length ? value : void 0;
|
|
377
|
-
}
|
|
378
|
-
return [value];
|
|
379
|
-
}
|
|
380
|
-
function decodeJwtPayload(token) {
|
|
381
|
-
if (!token) return void 0;
|
|
382
|
-
const parts = token.split(".");
|
|
383
|
-
if (parts.length < 2) return void 0;
|
|
384
|
-
const payloadSegment = parts[1];
|
|
385
|
-
if (!payloadSegment) return void 0;
|
|
386
|
-
if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
|
|
387
|
-
try {
|
|
388
|
-
const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
|
|
389
|
-
const bytes = base64ToUint8Array(normalized);
|
|
390
|
-
const json = new TextDecoder().decode(bytes);
|
|
391
|
-
const payload = JSON.parse(json);
|
|
392
|
-
return {
|
|
393
|
-
iss: payload.iss ?? null,
|
|
394
|
-
aud: payload.aud ?? null,
|
|
395
|
-
azp: payload?.azp ?? null,
|
|
396
|
-
exp: payload.exp ?? null,
|
|
397
|
-
iat: payload.iat ?? null
|
|
398
|
-
};
|
|
399
|
-
} catch {
|
|
400
|
-
return void 0;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
function nowMs() {
|
|
404
|
-
return Date.now();
|
|
405
|
-
}
|
|
406
|
-
function base64ToUint8Array(value) {
|
|
407
|
-
if (typeof atob === "function") {
|
|
408
|
-
const binary = atob(value);
|
|
409
|
-
const out = new Uint8Array(binary.length);
|
|
410
|
-
for (let i = 0; i < binary.length; i += 1) {
|
|
411
|
-
out[i] = binary.charCodeAt(i);
|
|
412
|
-
}
|
|
413
|
-
return out;
|
|
414
|
-
}
|
|
415
|
-
if (typeof Buffer !== "undefined") {
|
|
416
|
-
return Uint8Array.from(Buffer.from(value, "base64"));
|
|
417
|
-
}
|
|
418
|
-
throw new Error("No base64 decoder available in current environment");
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// src/server/auth/jwtVerifier.ts
|
|
422
|
-
var DEFAULT_CLOCK_SKEW_MS = 5e3;
|
|
423
|
-
function matchesAuthorizedParty(azp, patterns) {
|
|
424
|
-
if (!azp) return true;
|
|
425
|
-
for (const pattern of patterns) {
|
|
426
|
-
if (pattern === azp) return true;
|
|
427
|
-
if (pattern.includes("*")) {
|
|
428
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
429
|
-
const regexPattern = escaped.replace(/\*/g, "[^/]*");
|
|
430
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
431
|
-
if (regex.test(azp)) return true;
|
|
432
|
-
if (pattern.includes("*.")) {
|
|
433
|
-
const rootPattern = pattern.replace("*.", "");
|
|
434
|
-
if (rootPattern === azp) return true;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
var keyCache = /* @__PURE__ */ new Map();
|
|
441
|
-
async function getPublicKey(pem) {
|
|
442
|
-
const cached = keyCache.get(pem);
|
|
443
|
-
if (cached) return cached;
|
|
444
|
-
const normalized = pem.replace(/\\n/g, "\n");
|
|
445
|
-
const key = await importSPKI(normalized, "ES256");
|
|
446
|
-
keyCache.set(pem, key);
|
|
447
|
-
return key;
|
|
448
|
-
}
|
|
449
|
-
async function verifyJwt(config, token) {
|
|
450
|
-
if (!token) {
|
|
451
|
-
return { result: null };
|
|
452
|
-
}
|
|
453
|
-
try {
|
|
454
|
-
const publicKey = await getPublicKey(config.publicKey);
|
|
455
|
-
const audience = normalizeArray(config.audience);
|
|
456
|
-
const clockTolerance = Math.floor(
|
|
457
|
-
(config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
|
|
458
|
-
);
|
|
459
|
-
const { payload } = await jwtVerify(token, publicKey, {
|
|
460
|
-
issuer: config.issuer,
|
|
461
|
-
audience: audience && audience.length === 1 ? audience[0] : audience,
|
|
462
|
-
clockTolerance
|
|
463
|
-
});
|
|
464
|
-
const claims = payload;
|
|
465
|
-
if (!claims.sub) {
|
|
466
|
-
throw new Error("JWT verification succeeded but subject claim is missing");
|
|
467
|
-
}
|
|
468
|
-
const authorizedParties = normalizeArray(config.authorizedParties);
|
|
469
|
-
if (authorizedParties) {
|
|
470
|
-
const azp = claims.azp;
|
|
471
|
-
if (!matchesAuthorizedParty(azp, authorizedParties)) {
|
|
472
|
-
throw new Error(
|
|
473
|
-
`Authorized party '${azp}' does not match any allowed pattern`
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
result: {
|
|
479
|
-
userId: claims.sub,
|
|
480
|
-
claims
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
} catch (error) {
|
|
484
|
-
return {
|
|
485
|
-
result: null,
|
|
486
|
-
error,
|
|
487
|
-
debug: decodeJwtPayload(token)
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// src/server/auth/internalAuth.ts
|
|
493
|
-
var DEFAULT_MAX_SKEW_MS = 5 * 6e4;
|
|
494
|
-
var nodeCryptoPromise = null;
|
|
495
|
-
async function loadNodeCrypto() {
|
|
496
|
-
if (!nodeCryptoPromise) {
|
|
497
|
-
nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
|
|
498
|
-
}
|
|
499
|
-
return nodeCryptoPromise;
|
|
500
|
-
}
|
|
501
|
-
function hasSubtleCrypto() {
|
|
502
|
-
return typeof globalThis.crypto?.subtle?.importKey === "function";
|
|
503
|
-
}
|
|
504
|
-
async function computeHmacHex(secret, payload) {
|
|
505
|
-
if (hasSubtleCrypto()) {
|
|
506
|
-
const encoder = new TextEncoder();
|
|
507
|
-
const key = await crypto.subtle.importKey(
|
|
508
|
-
"raw",
|
|
509
|
-
encoder.encode(secret),
|
|
510
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
511
|
-
false,
|
|
512
|
-
["sign"]
|
|
513
|
-
);
|
|
514
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
515
|
-
return bufferToHex(signature);
|
|
516
|
-
}
|
|
517
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
518
|
-
if (!nodeCrypto) {
|
|
519
|
-
throw new Error("Crypto implementation unavailable for HMAC computation");
|
|
520
|
-
}
|
|
521
|
-
return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
522
|
-
}
|
|
523
|
-
async function timingSafeEqualHex(a, b) {
|
|
524
|
-
if (a.length !== b.length) {
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
|
-
const nodeCrypto = await loadNodeCrypto();
|
|
528
|
-
if (nodeCrypto?.timingSafeEqual) {
|
|
529
|
-
const aBuf = Buffer.from(a, "hex");
|
|
530
|
-
const bBuf = Buffer.from(b, "hex");
|
|
531
|
-
try {
|
|
532
|
-
return nodeCrypto.timingSafeEqual(aBuf, bBuf);
|
|
533
|
-
} catch {
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
let diff = 0;
|
|
538
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
539
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
540
|
-
}
|
|
541
|
-
return diff === 0;
|
|
542
|
-
}
|
|
543
|
-
async function signInternalPayload({
|
|
544
|
-
secret,
|
|
545
|
-
payload,
|
|
546
|
-
timestamp
|
|
547
|
-
}) {
|
|
548
|
-
const ts = timestamp ?? nowMs().toString();
|
|
549
|
-
const signature = await computeHmacHex(secret, `${ts}.${payload}`);
|
|
550
|
-
return { timestamp: ts, signature };
|
|
551
|
-
}
|
|
552
|
-
async function verifyInternalSignature({
|
|
553
|
-
secret,
|
|
554
|
-
timestamp,
|
|
555
|
-
signature,
|
|
556
|
-
payload,
|
|
557
|
-
maxSkewMs = DEFAULT_MAX_SKEW_MS
|
|
558
|
-
}) {
|
|
559
|
-
if (!secret || !timestamp || !signature) {
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
const tsNumber = Number(timestamp);
|
|
563
|
-
if (!Number.isFinite(tsNumber)) {
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
const skew = Math.abs(nowMs() - tsNumber);
|
|
567
|
-
if (skew > maxSkewMs) {
|
|
568
|
-
return false;
|
|
569
|
-
}
|
|
570
|
-
const expected = await computeHmacHex(secret, `${timestamp}.${payload}`);
|
|
571
|
-
return timingSafeEqualHex(signature.toLowerCase(), expected.toLowerCase());
|
|
572
|
-
}
|
|
573
|
-
function buildInternalPayload(body) {
|
|
574
|
-
if (body === void 0) return "{}";
|
|
575
|
-
if (typeof body === "string") return body;
|
|
576
|
-
try {
|
|
577
|
-
return JSON.stringify(body);
|
|
578
|
-
} catch {
|
|
579
|
-
return "{}";
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// src/server/auth/betterAuth.ts
|
|
584
|
-
import { betterAuth } from "better-auth";
|
|
585
|
-
import { organization, twoFactor } from "better-auth/plugins";
|
|
586
|
-
function createDeepSpaceAuth(config) {
|
|
587
|
-
const socialProviders = {};
|
|
588
|
-
if (config.google) {
|
|
589
|
-
socialProviders.google = config.google;
|
|
590
|
-
}
|
|
591
|
-
if (config.github) {
|
|
592
|
-
socialProviders.github = config.github;
|
|
593
|
-
}
|
|
594
|
-
return betterAuth({
|
|
595
|
-
database: config.database,
|
|
596
|
-
baseURL: config.baseURL,
|
|
597
|
-
secret: config.secret,
|
|
598
|
-
emailAndPassword: {
|
|
599
|
-
enabled: config.emailAndPassword ?? true
|
|
600
|
-
},
|
|
601
|
-
socialProviders,
|
|
602
|
-
trustedOrigins: config.trustedOrigins ?? [
|
|
603
|
-
"https://deep.space",
|
|
604
|
-
"https://*.deep.space",
|
|
605
|
-
"https://*.app.space",
|
|
606
|
-
"http://localhost:*"
|
|
607
|
-
],
|
|
608
|
-
plugins: [organization(), twoFactor()]
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// src/server/utils/cron.ts
|
|
613
|
-
var DEFAULT_API_BASE_URL = "https://api-worker.deep.space";
|
|
614
|
-
function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
615
|
-
const hmacSecret = env.INTERNAL_STORAGE_HMAC_SECRET;
|
|
616
|
-
const apiBase = env.API_BASE_URL || DEFAULT_API_BASE_URL;
|
|
617
|
-
const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
|
|
618
|
-
const room = env.RECORD_ROOMS.get(roomIdObj);
|
|
619
|
-
async function executeTool3(tool, params) {
|
|
620
|
-
const response = await room.fetch(new Request("https://internal/api/tools/execute", {
|
|
621
|
-
method: "POST",
|
|
622
|
-
headers: {
|
|
623
|
-
"Content-Type": "application/json",
|
|
624
|
-
"X-User-Id": ownerUserId,
|
|
625
|
-
"X-App-Action": "true"
|
|
626
|
-
},
|
|
627
|
-
body: JSON.stringify({ tool, params })
|
|
628
|
-
}));
|
|
629
|
-
const result = await response.json();
|
|
630
|
-
if (!result.success) {
|
|
631
|
-
throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
|
|
632
|
-
}
|
|
633
|
-
return result.data;
|
|
634
|
-
}
|
|
635
|
-
const records = {
|
|
636
|
-
async query(collection, opts) {
|
|
637
|
-
const params = { collection };
|
|
638
|
-
if (opts?.where) params.where = opts.where;
|
|
639
|
-
if (opts?.limit) params.limit = opts.limit;
|
|
640
|
-
const data = await executeTool3("records.query", params);
|
|
641
|
-
return data.records || [];
|
|
642
|
-
},
|
|
643
|
-
async create(collection, data) {
|
|
644
|
-
return executeTool3("records.create", { collection, data });
|
|
645
|
-
},
|
|
646
|
-
async update(collection, recordId, data) {
|
|
647
|
-
return executeTool3("records.update", { collection, recordId, data });
|
|
648
|
-
},
|
|
649
|
-
async delete(collection, recordId) {
|
|
650
|
-
return executeTool3("records.delete", { collection, recordId });
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
const integrations = {
|
|
654
|
-
async call(endpoint, params) {
|
|
655
|
-
if (!hmacSecret) {
|
|
656
|
-
throw new Error("INTERNAL_STORAGE_HMAC_SECRET not configured \u2014 cannot call integrations");
|
|
657
|
-
}
|
|
658
|
-
const payload = { userId: ownerUserId, ...params };
|
|
659
|
-
const payloadStr = buildInternalPayload(payload);
|
|
660
|
-
const { timestamp, signature } = await signInternalPayload({
|
|
661
|
-
secret: hmacSecret,
|
|
662
|
-
payload: payloadStr
|
|
663
|
-
});
|
|
664
|
-
const response = await fetch(`${apiBase}/api/internal/integrations/${endpoint}`, {
|
|
665
|
-
method: "POST",
|
|
666
|
-
headers: {
|
|
667
|
-
"Content-Type": "application/json",
|
|
668
|
-
"x-internal-timestamp": timestamp,
|
|
669
|
-
"x-internal-signature": signature
|
|
670
|
-
},
|
|
671
|
-
body: payloadStr
|
|
672
|
-
});
|
|
673
|
-
if (!response.ok) {
|
|
674
|
-
const errText = await response.text();
|
|
675
|
-
throw new Error(`Integration call ${endpoint} failed (${response.status}): ${errText}`);
|
|
676
|
-
}
|
|
677
|
-
return response.json();
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
return { records, integrations, ownerUserId };
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// src/server/utils/ai.ts
|
|
684
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
685
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
686
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
687
|
-
|
|
688
365
|
// src/server/utils/proxies.ts
|
|
689
366
|
function resolveApiTransport(env) {
|
|
690
367
|
if (env.API_WORKER) return { kind: "binding", fetcher: env.API_WORKER };
|
|
@@ -763,7 +440,79 @@ function authWorkerFetch(env, path, init) {
|
|
|
763
440
|
return fetch(`${baseUrl}${pathAndSearch}`, init);
|
|
764
441
|
}
|
|
765
442
|
|
|
443
|
+
// src/server/utils/cron.ts
|
|
444
|
+
function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
445
|
+
const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
|
|
446
|
+
const room = env.RECORD_ROOMS.get(roomIdObj);
|
|
447
|
+
async function executeTool3(tool, params) {
|
|
448
|
+
const response = await room.fetch(new Request("https://internal/api/tools/execute", {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: {
|
|
451
|
+
"Content-Type": "application/json",
|
|
452
|
+
"X-User-Id": ownerUserId,
|
|
453
|
+
"X-App-Action": "true"
|
|
454
|
+
},
|
|
455
|
+
body: JSON.stringify({ tool, params })
|
|
456
|
+
}));
|
|
457
|
+
const result = await response.json();
|
|
458
|
+
if (!result.success) {
|
|
459
|
+
throw new Error(`RecordRoom tool ${tool} failed: ${result.error || "Unknown error"}`);
|
|
460
|
+
}
|
|
461
|
+
return result.data;
|
|
462
|
+
}
|
|
463
|
+
const records = {
|
|
464
|
+
async query(collection, opts) {
|
|
465
|
+
const params = { collection };
|
|
466
|
+
if (opts?.where) params.where = opts.where;
|
|
467
|
+
if (opts?.limit) params.limit = opts.limit;
|
|
468
|
+
const data = await executeTool3("records.query", params);
|
|
469
|
+
return data.records || [];
|
|
470
|
+
},
|
|
471
|
+
async create(collection, data) {
|
|
472
|
+
return executeTool3("records.create", { collection, data });
|
|
473
|
+
},
|
|
474
|
+
async update(collection, recordId, data) {
|
|
475
|
+
return executeTool3("records.update", { collection, recordId, data });
|
|
476
|
+
},
|
|
477
|
+
async delete(collection, recordId) {
|
|
478
|
+
return executeTool3("records.delete", { collection, recordId });
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const integrations = {
|
|
482
|
+
async call(endpoint, params = {}) {
|
|
483
|
+
if (!env.APP_OWNER_JWT) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"integrations.call requires env.APP_OWNER_JWT. Deployed apps receive this automatically; in dev, `deepspace dev` writes it into .dev.vars."
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const response = await apiWorkerFetch(env, `/api/integrations/${endpoint}`, {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: {
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
Authorization: `Bearer ${env.APP_OWNER_JWT}`
|
|
493
|
+
},
|
|
494
|
+
body: JSON.stringify(params)
|
|
495
|
+
});
|
|
496
|
+
const text = await response.text();
|
|
497
|
+
let result = {};
|
|
498
|
+
try {
|
|
499
|
+
result = text ? JSON.parse(text) : {};
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
if (!response.ok || !result.success) {
|
|
503
|
+
const detail = result.message || result.error || text || `HTTP ${response.status}`;
|
|
504
|
+
throw new Error(`Integration call ${endpoint} failed: ${detail}`);
|
|
505
|
+
}
|
|
506
|
+
return result.data;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
return { records, integrations, ownerUserId };
|
|
510
|
+
}
|
|
511
|
+
|
|
766
512
|
// src/server/utils/ai.ts
|
|
513
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
514
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
515
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
767
516
|
var ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
|
768
517
|
function withDefaultAnthropicMaxTokens(body) {
|
|
769
518
|
if (typeof body !== "string") return body;
|
|
@@ -1566,6 +1315,13 @@ var MSG = {
|
|
|
1566
1315
|
USER_LIST: "user.list",
|
|
1567
1316
|
SET_ROLE: "user.set_role",
|
|
1568
1317
|
USER_UPDATE: "user.update",
|
|
1318
|
+
// ---- Auth signal -----------------------------------------------------
|
|
1319
|
+
// Server → client one-shot on connect from rooms that enforce role-based
|
|
1320
|
+
// writes (Canvas, Game, Cron). Tells the client whether this connection
|
|
1321
|
+
// can write so its hook can disable local write APIs and the UI can
|
|
1322
|
+
// render controls accordingly. Without this, a viewer's click would
|
|
1323
|
+
// round-trip to the server and silently come back as ERROR.
|
|
1324
|
+
AUTH: "auth",
|
|
1569
1325
|
// ---- Yjs JSON control frames (binary sync lives in ./yjs.ts) ---------
|
|
1570
1326
|
YJS_JOIN: "yjs.join",
|
|
1571
1327
|
YJS_LEAVE: "yjs.leave",
|
|
@@ -4201,7 +3957,20 @@ var YjsRoom = class extends BaseRoom {
|
|
|
4201
3957
|
}
|
|
4202
3958
|
};
|
|
4203
3959
|
|
|
3960
|
+
// src/shared/roles.ts
|
|
3961
|
+
var ROLES = {
|
|
3962
|
+
VIEWER: "viewer",
|
|
3963
|
+
MEMBER: "member",
|
|
3964
|
+
ADMIN: "admin"
|
|
3965
|
+
};
|
|
3966
|
+
|
|
4204
3967
|
// src/server/rooms/game-room.ts
|
|
3968
|
+
var GAME_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
3969
|
+
MSG.GAME_INPUT,
|
|
3970
|
+
MSG.GAME_PLAYER_READY,
|
|
3971
|
+
MSG.GAME_START,
|
|
3972
|
+
MSG.GAME_END
|
|
3973
|
+
]);
|
|
4205
3974
|
var GameRoom = class extends BaseRoom {
|
|
4206
3975
|
config;
|
|
4207
3976
|
players = /* @__PURE__ */ new Map();
|
|
@@ -4257,10 +4026,26 @@ var GameRoom = class extends BaseRoom {
|
|
|
4257
4026
|
// ==========================================================================
|
|
4258
4027
|
onConnect(ws, user) {
|
|
4259
4028
|
this.ensureInitialized();
|
|
4029
|
+
const role = user.role ?? ROLES.VIEWER;
|
|
4030
|
+
const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
|
|
4260
4031
|
const attachment = {
|
|
4261
4032
|
...user,
|
|
4262
|
-
joinedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4033
|
+
joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4034
|
+
canWrite
|
|
4263
4035
|
};
|
|
4036
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
4037
|
+
if (!canWrite) {
|
|
4038
|
+
this.sendTo(ws, {
|
|
4039
|
+
type: MSG.GAME_STATE,
|
|
4040
|
+
payload: {
|
|
4041
|
+
state: this.gameState,
|
|
4042
|
+
tick: this.currentTick,
|
|
4043
|
+
players: Array.from(this.players.values()),
|
|
4044
|
+
running: this.running
|
|
4045
|
+
}
|
|
4046
|
+
});
|
|
4047
|
+
return attachment;
|
|
4048
|
+
}
|
|
4264
4049
|
const player = {
|
|
4265
4050
|
userId: user.userId,
|
|
4266
4051
|
userName: user.userName,
|
|
@@ -4289,6 +4074,13 @@ var GameRoom = class extends BaseRoom {
|
|
|
4289
4074
|
async onMessage(ws, user, message) {
|
|
4290
4075
|
this.ensureInitialized();
|
|
4291
4076
|
const { type, payload } = message;
|
|
4077
|
+
if (GAME_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
4078
|
+
this.sendTo(ws, {
|
|
4079
|
+
type: MSG.ERROR,
|
|
4080
|
+
payload: { error: "Write access denied: spectators cannot drive the game" }
|
|
4081
|
+
});
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4292
4084
|
switch (type) {
|
|
4293
4085
|
case MSG.GAME_INPUT: {
|
|
4294
4086
|
this.inputBuffer.push({
|
|
@@ -4437,6 +4229,15 @@ var GameRoom = class extends BaseRoom {
|
|
|
4437
4229
|
|
|
4438
4230
|
// src/server/rooms/canvas-room.ts
|
|
4439
4231
|
import * as Y5 from "yjs";
|
|
4232
|
+
var CANVAS_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
4233
|
+
MSG.CANVAS_ADD,
|
|
4234
|
+
MSG.CANVAS_MOVE,
|
|
4235
|
+
MSG.CANVAS_RESIZE,
|
|
4236
|
+
MSG.CANVAS_DELETE,
|
|
4237
|
+
MSG.CANVAS_UPDATE,
|
|
4238
|
+
MSG.CANVAS_UNDO,
|
|
4239
|
+
MSG.CANVAS_REDO
|
|
4240
|
+
]);
|
|
4440
4241
|
var CanvasRoom = class extends BaseRoom {
|
|
4441
4242
|
doc = null;
|
|
4442
4243
|
initialized = false;
|
|
@@ -4496,10 +4297,14 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
4496
4297
|
}
|
|
4497
4298
|
onConnect(ws, user) {
|
|
4498
4299
|
this.ensureInitialized();
|
|
4300
|
+
const role = user.role ?? ROLES.VIEWER;
|
|
4301
|
+
const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
|
|
4499
4302
|
const attachment = {
|
|
4500
4303
|
...user,
|
|
4501
|
-
viewport: null
|
|
4304
|
+
viewport: null,
|
|
4305
|
+
canWrite
|
|
4502
4306
|
};
|
|
4307
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
4503
4308
|
const shapes = this.getAllShapes();
|
|
4504
4309
|
this.sendTo(ws, {
|
|
4505
4310
|
type: MSG.CANVAS_SHAPES,
|
|
@@ -4513,6 +4318,13 @@ var CanvasRoom = class extends BaseRoom {
|
|
|
4513
4318
|
async onMessage(ws, user, message) {
|
|
4514
4319
|
this.ensureInitialized();
|
|
4515
4320
|
const { type, payload } = message;
|
|
4321
|
+
if (CANVAS_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
4322
|
+
this.sendTo(ws, {
|
|
4323
|
+
type: MSG.ERROR,
|
|
4324
|
+
payload: { error: "Write access denied: viewer role cannot modify the canvas" }
|
|
4325
|
+
});
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4516
4328
|
switch (type) {
|
|
4517
4329
|
case MSG.CANVAS_ADD: {
|
|
4518
4330
|
const shape = payload;
|
|
@@ -4807,6 +4619,11 @@ var PresenceRoom = class extends BaseRoom {
|
|
|
4807
4619
|
};
|
|
4808
4620
|
|
|
4809
4621
|
// src/server/rooms/cron-room.ts
|
|
4622
|
+
var CRON_WRITE_TYPES = /* @__PURE__ */ new Set([
|
|
4623
|
+
MSG.CRON_TRIGGER,
|
|
4624
|
+
MSG.CRON_PAUSE,
|
|
4625
|
+
MSG.CRON_RESUME
|
|
4626
|
+
]);
|
|
4810
4627
|
var CronRoom = class extends BaseRoom {
|
|
4811
4628
|
tasks;
|
|
4812
4629
|
initialized = false;
|
|
@@ -4885,6 +4702,9 @@ var CronRoom = class extends BaseRoom {
|
|
|
4885
4702
|
}
|
|
4886
4703
|
onConnect(ws, user) {
|
|
4887
4704
|
this.ensureInitialized();
|
|
4705
|
+
const role = user.role ?? ROLES.VIEWER;
|
|
4706
|
+
const canWrite = role === ROLES.MEMBER || role === ROLES.ADMIN;
|
|
4707
|
+
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
4888
4708
|
this.sendTo(ws, {
|
|
4889
4709
|
type: MSG.CRON_TASKS,
|
|
4890
4710
|
payload: { tasks: this.getTaskStates() }
|
|
@@ -4893,11 +4713,18 @@ var CronRoom = class extends BaseRoom {
|
|
|
4893
4713
|
type: MSG.CRON_HISTORY,
|
|
4894
4714
|
payload: { history: this.getRecentHistory(50) }
|
|
4895
4715
|
});
|
|
4896
|
-
return user;
|
|
4716
|
+
return { ...user, canWrite };
|
|
4897
4717
|
}
|
|
4898
4718
|
async onMessage(ws, user, message) {
|
|
4899
4719
|
this.ensureInitialized();
|
|
4900
4720
|
const { type, payload } = message;
|
|
4721
|
+
if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
4722
|
+
this.sendTo(ws, {
|
|
4723
|
+
type: MSG.ERROR,
|
|
4724
|
+
payload: { error: "Write access denied: viewer role cannot modify cron tasks" }
|
|
4725
|
+
});
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4901
4728
|
switch (type) {
|
|
4902
4729
|
case MSG.CRON_TRIGGER: {
|
|
4903
4730
|
const taskName = payload.taskName;
|
|
@@ -5825,7 +5652,6 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
|
|
|
5825
5652
|
"AUTH_WORKER_URL",
|
|
5826
5653
|
"APP_IDENTITY_TOKEN",
|
|
5827
5654
|
"APP_OWNER_JWT",
|
|
5828
|
-
"INTERNAL_STORAGE_HMAC_SECRET",
|
|
5829
5655
|
"USAGE_EVENTS"
|
|
5830
5656
|
]);
|
|
5831
5657
|
function validateBindingManifest(manifest) {
|
|
@@ -6657,6 +6483,215 @@ var RESERVED_COLLECTION_NAMES = new Set(
|
|
|
6657
6483
|
GLOBAL_DO_TYPES.flatMap((t) => t.schemas.map((s) => s.name))
|
|
6658
6484
|
);
|
|
6659
6485
|
|
|
6486
|
+
// src/server/auth/jwtVerifier.ts
|
|
6487
|
+
import { importSPKI, jwtVerify } from "jose";
|
|
6488
|
+
|
|
6489
|
+
// src/server/auth/utils.ts
|
|
6490
|
+
var BASE64_URL_REGEX = /^[A-Za-z0-9-_]+$/;
|
|
6491
|
+
function bufferToHex(bytes) {
|
|
6492
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
6493
|
+
let hex = "";
|
|
6494
|
+
for (const byte of view) {
|
|
6495
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
6496
|
+
}
|
|
6497
|
+
return hex;
|
|
6498
|
+
}
|
|
6499
|
+
function normalizeArray(value) {
|
|
6500
|
+
if (value == null) return void 0;
|
|
6501
|
+
if (Array.isArray(value)) {
|
|
6502
|
+
return value.length ? value : void 0;
|
|
6503
|
+
}
|
|
6504
|
+
return [value];
|
|
6505
|
+
}
|
|
6506
|
+
function decodeJwtPayload(token) {
|
|
6507
|
+
if (!token) return void 0;
|
|
6508
|
+
const parts = token.split(".");
|
|
6509
|
+
if (parts.length < 2) return void 0;
|
|
6510
|
+
const payloadSegment = parts[1];
|
|
6511
|
+
if (!payloadSegment) return void 0;
|
|
6512
|
+
if (!BASE64_URL_REGEX.test(payloadSegment)) return void 0;
|
|
6513
|
+
try {
|
|
6514
|
+
const normalized = payloadSegment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
|
|
6515
|
+
const bytes = base64ToUint8Array(normalized);
|
|
6516
|
+
const json = new TextDecoder().decode(bytes);
|
|
6517
|
+
const payload = JSON.parse(json);
|
|
6518
|
+
return {
|
|
6519
|
+
iss: payload.iss ?? null,
|
|
6520
|
+
aud: payload.aud ?? null,
|
|
6521
|
+
azp: payload?.azp ?? null,
|
|
6522
|
+
exp: payload.exp ?? null,
|
|
6523
|
+
iat: payload.iat ?? null
|
|
6524
|
+
};
|
|
6525
|
+
} catch {
|
|
6526
|
+
return void 0;
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
function base64ToUint8Array(value) {
|
|
6530
|
+
if (typeof atob === "function") {
|
|
6531
|
+
const binary = atob(value);
|
|
6532
|
+
const out = new Uint8Array(binary.length);
|
|
6533
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
6534
|
+
out[i] = binary.charCodeAt(i);
|
|
6535
|
+
}
|
|
6536
|
+
return out;
|
|
6537
|
+
}
|
|
6538
|
+
if (typeof Buffer !== "undefined") {
|
|
6539
|
+
return Uint8Array.from(Buffer.from(value, "base64"));
|
|
6540
|
+
}
|
|
6541
|
+
throw new Error("No base64 decoder available in current environment");
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
// src/server/auth/jwtVerifier.ts
|
|
6545
|
+
var DEFAULT_CLOCK_SKEW_MS = 5e3;
|
|
6546
|
+
function matchesAuthorizedParty(azp, patterns) {
|
|
6547
|
+
if (!azp) return true;
|
|
6548
|
+
for (const pattern of patterns) {
|
|
6549
|
+
if (pattern === azp) return true;
|
|
6550
|
+
if (pattern.includes("*")) {
|
|
6551
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
6552
|
+
const regexPattern = escaped.replace(/\*/g, "[^/]*");
|
|
6553
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
6554
|
+
if (regex.test(azp)) return true;
|
|
6555
|
+
if (pattern.includes("*.")) {
|
|
6556
|
+
const rootPattern = pattern.replace("*.", "");
|
|
6557
|
+
if (rootPattern === azp) return true;
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
return false;
|
|
6562
|
+
}
|
|
6563
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
6564
|
+
async function getPublicKey(pem) {
|
|
6565
|
+
const cached = keyCache.get(pem);
|
|
6566
|
+
if (cached) return cached;
|
|
6567
|
+
const normalized = pem.replace(/\\n/g, "\n");
|
|
6568
|
+
const key = await importSPKI(normalized, "ES256");
|
|
6569
|
+
keyCache.set(pem, key);
|
|
6570
|
+
return key;
|
|
6571
|
+
}
|
|
6572
|
+
async function verifyJwt(config, token) {
|
|
6573
|
+
if (!token) {
|
|
6574
|
+
return { result: null };
|
|
6575
|
+
}
|
|
6576
|
+
try {
|
|
6577
|
+
const publicKey = await getPublicKey(config.publicKey);
|
|
6578
|
+
const audience = normalizeArray(config.audience);
|
|
6579
|
+
const clockTolerance = Math.floor(
|
|
6580
|
+
(config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS) / 1e3
|
|
6581
|
+
);
|
|
6582
|
+
const { payload } = await jwtVerify(token, publicKey, {
|
|
6583
|
+
issuer: config.issuer,
|
|
6584
|
+
audience: audience && audience.length === 1 ? audience[0] : audience,
|
|
6585
|
+
clockTolerance
|
|
6586
|
+
});
|
|
6587
|
+
const claims = payload;
|
|
6588
|
+
if (!claims.sub) {
|
|
6589
|
+
throw new Error("JWT verification succeeded but subject claim is missing");
|
|
6590
|
+
}
|
|
6591
|
+
const authorizedParties = normalizeArray(config.authorizedParties);
|
|
6592
|
+
if (authorizedParties) {
|
|
6593
|
+
const azp = claims.azp;
|
|
6594
|
+
if (!matchesAuthorizedParty(azp, authorizedParties)) {
|
|
6595
|
+
throw new Error(
|
|
6596
|
+
`Authorized party '${azp}' does not match any allowed pattern`
|
|
6597
|
+
);
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
return {
|
|
6601
|
+
result: {
|
|
6602
|
+
userId: claims.sub,
|
|
6603
|
+
claims
|
|
6604
|
+
}
|
|
6605
|
+
};
|
|
6606
|
+
} catch (error) {
|
|
6607
|
+
return {
|
|
6608
|
+
result: null,
|
|
6609
|
+
error,
|
|
6610
|
+
debug: decodeJwtPayload(token)
|
|
6611
|
+
};
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
// src/server/auth/internalAuth.ts
|
|
6616
|
+
var nodeCryptoPromise = null;
|
|
6617
|
+
async function loadNodeCrypto() {
|
|
6618
|
+
if (!nodeCryptoPromise) {
|
|
6619
|
+
nodeCryptoPromise = import("crypto").then((module) => module).catch(() => null);
|
|
6620
|
+
}
|
|
6621
|
+
return nodeCryptoPromise;
|
|
6622
|
+
}
|
|
6623
|
+
function hasSubtleCrypto() {
|
|
6624
|
+
return typeof globalThis.crypto?.subtle?.importKey === "function";
|
|
6625
|
+
}
|
|
6626
|
+
async function computeHmacHex(secret, payload) {
|
|
6627
|
+
if (hasSubtleCrypto()) {
|
|
6628
|
+
const encoder = new TextEncoder();
|
|
6629
|
+
const key = await crypto.subtle.importKey(
|
|
6630
|
+
"raw",
|
|
6631
|
+
encoder.encode(secret),
|
|
6632
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
6633
|
+
false,
|
|
6634
|
+
["sign"]
|
|
6635
|
+
);
|
|
6636
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
6637
|
+
return bufferToHex(signature);
|
|
6638
|
+
}
|
|
6639
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
6640
|
+
if (!nodeCrypto) {
|
|
6641
|
+
throw new Error("Crypto implementation unavailable for HMAC computation");
|
|
6642
|
+
}
|
|
6643
|
+
return nodeCrypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
6644
|
+
}
|
|
6645
|
+
async function timingSafeEqualHex(a, b) {
|
|
6646
|
+
if (a.length !== b.length) {
|
|
6647
|
+
return false;
|
|
6648
|
+
}
|
|
6649
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
6650
|
+
if (nodeCrypto?.timingSafeEqual) {
|
|
6651
|
+
const aBuf = Buffer.from(a, "hex");
|
|
6652
|
+
const bBuf = Buffer.from(b, "hex");
|
|
6653
|
+
try {
|
|
6654
|
+
return nodeCrypto.timingSafeEqual(aBuf, bBuf);
|
|
6655
|
+
} catch {
|
|
6656
|
+
return false;
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
let diff = 0;
|
|
6660
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
6661
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
6662
|
+
}
|
|
6663
|
+
return diff === 0;
|
|
6664
|
+
}
|
|
6665
|
+
|
|
6666
|
+
// src/server/auth/betterAuth.ts
|
|
6667
|
+
import { betterAuth } from "better-auth";
|
|
6668
|
+
import { organization, twoFactor } from "better-auth/plugins";
|
|
6669
|
+
function createDeepSpaceAuth(config) {
|
|
6670
|
+
const socialProviders = {};
|
|
6671
|
+
if (config.google) {
|
|
6672
|
+
socialProviders.google = config.google;
|
|
6673
|
+
}
|
|
6674
|
+
if (config.github) {
|
|
6675
|
+
socialProviders.github = config.github;
|
|
6676
|
+
}
|
|
6677
|
+
return betterAuth({
|
|
6678
|
+
database: config.database,
|
|
6679
|
+
baseURL: config.baseURL,
|
|
6680
|
+
secret: config.secret,
|
|
6681
|
+
emailAndPassword: {
|
|
6682
|
+
enabled: config.emailAndPassword ?? true
|
|
6683
|
+
},
|
|
6684
|
+
socialProviders,
|
|
6685
|
+
trustedOrigins: config.trustedOrigins ?? [
|
|
6686
|
+
"https://deep.space",
|
|
6687
|
+
"https://*.deep.space",
|
|
6688
|
+
"https://*.app.space",
|
|
6689
|
+
"http://localhost:*"
|
|
6690
|
+
],
|
|
6691
|
+
plugins: [organization(), twoFactor()]
|
|
6692
|
+
});
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6660
6695
|
// src/shared/subscription.ts
|
|
6661
6696
|
var ENTITLED_STATUSES = /* @__PURE__ */ new Set([
|
|
6662
6697
|
"active",
|
|
@@ -6821,7 +6856,6 @@ export {
|
|
|
6821
6856
|
CronRoom,
|
|
6822
6857
|
DEFAULT_CONTEXT_CONFIG,
|
|
6823
6858
|
DEFAULT_DO_MANIFEST,
|
|
6824
|
-
DEFAULT_MAX_SKEW_MS,
|
|
6825
6859
|
DIRECTORY_SCHEMAS,
|
|
6826
6860
|
GLOBAL_DO_TYPES,
|
|
6827
6861
|
GLOBAL_DO_TYPE_NAMES,
|
|
@@ -6853,7 +6887,6 @@ export {
|
|
|
6853
6887
|
broadcastChange,
|
|
6854
6888
|
broadcastYjsUpdate,
|
|
6855
6889
|
buildCronContext,
|
|
6856
|
-
buildInternalPayload,
|
|
6857
6890
|
buildTableSelect,
|
|
6858
6891
|
buildUiParts,
|
|
6859
6892
|
canCreate,
|
|
@@ -6924,7 +6957,6 @@ export {
|
|
|
6924
6957
|
rowToData,
|
|
6925
6958
|
runMigrations,
|
|
6926
6959
|
saveYjsDoc,
|
|
6927
|
-
signInternalPayload,
|
|
6928
6960
|
timingSafeEqualHex,
|
|
6929
6961
|
totalChars,
|
|
6930
6962
|
truncateOldToolResults,
|
|
@@ -6934,7 +6966,6 @@ export {
|
|
|
6934
6966
|
validateAppName,
|
|
6935
6967
|
validateBindingManifest,
|
|
6936
6968
|
validateDoManifest,
|
|
6937
|
-
verifyInternalSignature,
|
|
6938
6969
|
verifyJwt,
|
|
6939
6970
|
workspaceAccountsSchema,
|
|
6940
6971
|
workspaceContentSharesSchema,
|