deepspace 0.19.0 → 0.19.2
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/CHANGELOG.md +32 -0
- package/dist/cli.js +558 -261
- package/dist/cli.js.map +1 -1
- package/dist/feature-installer.js +14 -5
- package/dist/feature-installer.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +12 -7
- package/dist/server.js +63 -19
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +9 -2
- package/dist/worker.js +63 -19
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +3 -4
- package/features/admin-page/src/AdminPage.tsx +4 -4
- package/features/cron/feature.json +8 -5
- package/features/documents/feature.json +2 -1
- package/features/documents/src/DocumentsEditorPage.tsx +31 -10
- package/features/documents/src/DocumentsListPage.tsx +59 -26
- package/features/documents/src/InviteDialog.tsx +49 -14
- package/features/documents/src/LibrarySidebar.tsx +92 -84
- package/features/documents/src/__tests__/documents-presence-access.test.ts +15 -2
- package/features/documents/src/documents-library-types.ts +6 -0
- package/features/tasks/src/TasksPage.tsx +1 -1
- package/features/tasks/src/tasks-schema.ts +1 -1
- package/package.json +5 -5
- package/features/cron/src/cron.ts +0 -25
package/dist/worker.d.ts
CHANGED
|
@@ -1000,7 +1000,7 @@ interface RolePermissions {
|
|
|
1000
1000
|
create: boolean;
|
|
1001
1001
|
update: PermissionLevel;
|
|
1002
1002
|
delete: PermissionLevel;
|
|
1003
|
-
/** If set, only these columns can be updated by this role. */
|
|
1003
|
+
/** If set, only these caller-supplied columns can be created or updated by this role. */
|
|
1004
1004
|
writableFields?: string[];
|
|
1005
1005
|
}
|
|
1006
1006
|
interface CollectionSchema {
|
|
@@ -1238,7 +1238,7 @@ declare function canDelete(schema: CollectionSchema, role: string, record: {
|
|
|
1238
1238
|
createdBy: string;
|
|
1239
1239
|
recordId?: string;
|
|
1240
1240
|
}, userId: string, ctx?: PermissionContext): boolean;
|
|
1241
|
-
/** Check if a field
|
|
1241
|
+
/** Check if a caller-supplied field write violates writableFields restrictions. */
|
|
1242
1242
|
declare function checkFieldPermissions(schema: CollectionSchema, role: string, newData: Record<string, unknown>, existingData?: Record<string, unknown>): string | null;
|
|
1243
1243
|
/**
|
|
1244
1244
|
* `unclaimed-or-own` lets a client claim an empty owner field, but never lets
|
|
@@ -1678,6 +1678,8 @@ interface JobRoomConfig {
|
|
|
1678
1678
|
retryBackoffMs?: number;
|
|
1679
1679
|
/** Re-check whether a connected user may mutate the queue. Defaults to member/admin roles. */
|
|
1680
1680
|
authorizeWrite?: (user: UserAttachment) => boolean | Promise<boolean>;
|
|
1681
|
+
/** Re-check whether a connected user may observe the queue. Defaults to authorizeWrite. */
|
|
1682
|
+
authorizeRead?: (user: UserAttachment) => boolean | Promise<boolean>;
|
|
1681
1683
|
}
|
|
1682
1684
|
interface JobAttachment extends UserAttachment {
|
|
1683
1685
|
canWrite: boolean;
|
|
@@ -1689,6 +1691,8 @@ declare abstract class JobRoom<E = Record<string, unknown>, P = unknown, R = unk
|
|
|
1689
1691
|
private readonly snapshotLimit;
|
|
1690
1692
|
private readonly retryBackoffMs;
|
|
1691
1693
|
private readonly authorizeWrite;
|
|
1694
|
+
private readonly authorizeRead;
|
|
1695
|
+
private broadcastQueue;
|
|
1692
1696
|
/** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
|
|
1693
1697
|
private readonly inFlight;
|
|
1694
1698
|
/** Set by `ctx.continue` inside `executeJob`; reset before every run. */
|
|
@@ -1742,6 +1746,9 @@ declare abstract class JobRoom<E = Record<string, unknown>, P = unknown, R = unk
|
|
|
1742
1746
|
private executeJob;
|
|
1743
1747
|
private makeContext;
|
|
1744
1748
|
private readStatus;
|
|
1749
|
+
/** Reauthorize every live event so role revocation does not leave a stale reader. */
|
|
1750
|
+
protected broadcast(message: ServerMessage, exclude?: WebSocket): void;
|
|
1751
|
+
private resolveAccess;
|
|
1745
1752
|
private scheduleNextAlarm;
|
|
1746
1753
|
private recoverStuckRunning;
|
|
1747
1754
|
private pruneExpired;
|
package/dist/worker.js
CHANGED
|
@@ -1318,10 +1318,6 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
|
|
|
1318
1318
|
error: `UPDATE DENIED: role=${userRole}, collection=${collection}`
|
|
1319
1319
|
};
|
|
1320
1320
|
}
|
|
1321
|
-
const fieldError = checkFieldPermissions(schema, userRole, finalData, existing.data);
|
|
1322
|
-
if (fieldError) {
|
|
1323
|
-
return { success: false, error: `FIELD ERROR: ${fieldError}` };
|
|
1324
|
-
}
|
|
1325
1321
|
} else {
|
|
1326
1322
|
if (!canCreate(schema, userRole)) {
|
|
1327
1323
|
return {
|
|
@@ -1330,6 +1326,15 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
|
|
|
1330
1326
|
};
|
|
1331
1327
|
}
|
|
1332
1328
|
}
|
|
1329
|
+
const fieldError = checkFieldPermissions(
|
|
1330
|
+
schema,
|
|
1331
|
+
userRole,
|
|
1332
|
+
data,
|
|
1333
|
+
isUpdate ? existing.data : void 0
|
|
1334
|
+
);
|
|
1335
|
+
if (fieldError) {
|
|
1336
|
+
return { success: false, error: `FIELD ERROR: ${fieldError}` };
|
|
1337
|
+
}
|
|
1333
1338
|
const ownerError = checkUnclaimedOwnerTransition(schema, userRole, finalData, userId);
|
|
1334
1339
|
if (ownerError) {
|
|
1335
1340
|
return { success: false, error: `FIELD ERROR: ${ownerError}` };
|
|
@@ -1987,6 +1992,7 @@ function getAllUserRecords(sql, schemaRegistry) {
|
|
|
1987
1992
|
const r = row;
|
|
1988
1993
|
return {
|
|
1989
1994
|
recordId: r._row_id,
|
|
1995
|
+
createdBy: r._created_by,
|
|
1990
1996
|
data: columns.length > 0 ? rowToUserRecord(r, columns) : {
|
|
1991
1997
|
email: r.col_email || "",
|
|
1992
1998
|
name: r.col_name || "",
|
|
@@ -2048,12 +2054,28 @@ async function registerUser(sql, userId, name, email, imageUrl, isAdmin, default
|
|
|
2048
2054
|
);
|
|
2049
2055
|
return { id: userId, email, name, imageUrl, role, createdAt: now, lastSeenAt: now };
|
|
2050
2056
|
}
|
|
2051
|
-
function handleUserList(ctx, ws,
|
|
2052
|
-
const
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
+
function handleUserList(ctx, ws, attachment) {
|
|
2058
|
+
const schema = ctx.schemaRegistry.get("users");
|
|
2059
|
+
const permissionContext = ctx.getPermissionContext();
|
|
2060
|
+
const visibleRecords = attachment.userId.startsWith("anon-") || !schema ? [] : getAllUserRecords(ctx.sql, ctx.schemaRegistry).filter(
|
|
2061
|
+
(record) => canRead(
|
|
2062
|
+
schema,
|
|
2063
|
+
attachment.role,
|
|
2064
|
+
record,
|
|
2065
|
+
attachment.userId,
|
|
2066
|
+
permissionContext
|
|
2067
|
+
)
|
|
2068
|
+
);
|
|
2069
|
+
const users = visibleRecords.map((record) => {
|
|
2070
|
+
if (attachment.role === "admin") return { id: record.recordId, ...record.data };
|
|
2071
|
+
const { name, imageUrl, role } = record.data;
|
|
2072
|
+
return {
|
|
2073
|
+
id: record.recordId,
|
|
2074
|
+
name,
|
|
2075
|
+
...imageUrl ? { imageUrl } : {},
|
|
2076
|
+
role
|
|
2077
|
+
};
|
|
2078
|
+
});
|
|
2057
2079
|
ctx.send(ws, { type: MSG.USER_LIST, payload: { users } });
|
|
2058
2080
|
}
|
|
2059
2081
|
function handleUserUpdate(ctx, _ws, attachment, payload) {
|
|
@@ -2082,11 +2104,6 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
2082
2104
|
now,
|
|
2083
2105
|
payload.userId
|
|
2084
2106
|
);
|
|
2085
|
-
const userRecords = getAllUserRecords(ctx.sql, ctx.schemaRegistry);
|
|
2086
|
-
const users = userRecords.map((r) => ({
|
|
2087
|
-
id: r.recordId,
|
|
2088
|
-
...r.data
|
|
2089
|
-
}));
|
|
2090
2107
|
for (const otherWs of ctx.state.getWebSockets()) {
|
|
2091
2108
|
const otherAttachment = otherWs.deserializeAttachment();
|
|
2092
2109
|
if (!otherAttachment) continue;
|
|
@@ -2098,7 +2115,7 @@ async function handleSetRole(ctx, ws, attachment, payload) {
|
|
|
2098
2115
|
continue;
|
|
2099
2116
|
}
|
|
2100
2117
|
if (otherAttachment.role === "admin") {
|
|
2101
|
-
ctx
|
|
2118
|
+
handleUserList(ctx, otherWs, otherAttachment);
|
|
2102
2119
|
}
|
|
2103
2120
|
}
|
|
2104
2121
|
}
|
|
@@ -3077,6 +3094,7 @@ var RecordRoom = class extends BaseRoom {
|
|
|
3077
3094
|
sql: this.sql,
|
|
3078
3095
|
state: this.state,
|
|
3079
3096
|
schemaRegistry: this.schemaRegistry,
|
|
3097
|
+
getPermissionContext: () => this.getPermissionContext(),
|
|
3080
3098
|
send: (ws, msg) => this.send(ws, msg)
|
|
3081
3099
|
};
|
|
3082
3100
|
}
|
|
@@ -4179,6 +4197,8 @@ var JobRoom = class extends BaseRoom {
|
|
|
4179
4197
|
snapshotLimit;
|
|
4180
4198
|
retryBackoffMs;
|
|
4181
4199
|
authorizeWrite;
|
|
4200
|
+
authorizeRead;
|
|
4201
|
+
broadcastQueue = Promise.resolve();
|
|
4182
4202
|
/** In-flight jobs in this isolate; lets same-isolate JOB_CANCEL fire the signal. */
|
|
4183
4203
|
inFlight = /* @__PURE__ */ new Map();
|
|
4184
4204
|
/** Set by `ctx.continue` inside `executeJob`; reset before every run. */
|
|
@@ -4190,6 +4210,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
4190
4210
|
this.snapshotLimit = Math.max(1, config.snapshotLimit ?? DEFAULT_SNAPSHOT_LIMIT);
|
|
4191
4211
|
this.retryBackoffMs = Math.max(0, config.retryBackoffMs ?? 1e3);
|
|
4192
4212
|
this.authorizeWrite = config.authorizeWrite ?? ((user) => user.role === ROLES.MEMBER || user.role === ROLES.ADMIN);
|
|
4213
|
+
this.authorizeRead = config.authorizeRead ?? this.authorizeWrite;
|
|
4193
4214
|
}
|
|
4194
4215
|
ensureInitialized() {
|
|
4195
4216
|
if (this.initialized) return;
|
|
@@ -4232,9 +4253,9 @@ var JobRoom = class extends BaseRoom {
|
|
|
4232
4253
|
}
|
|
4233
4254
|
async onConnect(ws, user) {
|
|
4234
4255
|
this.ensureInitialized();
|
|
4235
|
-
const canWrite = await this.
|
|
4256
|
+
const { canRead: canRead2, canWrite } = await this.resolveAccess(user);
|
|
4236
4257
|
this.sendTo(ws, { type: MSG.AUTH, payload: { canWrite } });
|
|
4237
|
-
this.sendTo(ws, serverBuild.jobSnapshot(this.getRecentJobs(this.snapshotLimit)));
|
|
4258
|
+
this.sendTo(ws, serverBuild.jobSnapshot(canRead2 ? this.getRecentJobs(this.snapshotLimit) : []));
|
|
4238
4259
|
return { ...user, canWrite };
|
|
4239
4260
|
}
|
|
4240
4261
|
async onMessage(ws, user, message) {
|
|
@@ -4242,7 +4263,7 @@ var JobRoom = class extends BaseRoom {
|
|
|
4242
4263
|
const { type, payload } = message;
|
|
4243
4264
|
if (type === MSG.JOB_ENQUEUE || type === MSG.JOB_CANCEL || type === MSG.JOB_RETRY) {
|
|
4244
4265
|
const attachment = user;
|
|
4245
|
-
const canWrite = await this.
|
|
4266
|
+
const { canWrite } = await this.resolveAccess(user);
|
|
4246
4267
|
if (attachment.canWrite !== canWrite) {
|
|
4247
4268
|
attachment.canWrite = canWrite;
|
|
4248
4269
|
ws.serializeAttachment(attachment);
|
|
@@ -4597,6 +4618,29 @@ var JobRoom = class extends BaseRoom {
|
|
|
4597
4618
|
const row = this.sql.exec(`SELECT status FROM jobs WHERE id = ?`, jobId).toArray()[0];
|
|
4598
4619
|
return row?.status ?? null;
|
|
4599
4620
|
}
|
|
4621
|
+
/** Reauthorize every live event so role revocation does not leave a stale reader. */
|
|
4622
|
+
broadcast(message, exclude) {
|
|
4623
|
+
this.broadcastQueue = this.broadcastQueue.then(async () => {
|
|
4624
|
+
await Promise.all(
|
|
4625
|
+
this.getWebSockets().map(async (ws) => {
|
|
4626
|
+
if (ws === exclude) return;
|
|
4627
|
+
try {
|
|
4628
|
+
const user = this.getAttachment(ws);
|
|
4629
|
+
if (!user) return;
|
|
4630
|
+
if (await this.authorizeRead(user)) this.sendTo(ws, message);
|
|
4631
|
+
} catch {
|
|
4632
|
+
}
|
|
4633
|
+
})
|
|
4634
|
+
);
|
|
4635
|
+
});
|
|
4636
|
+
this.state.waitUntil(this.broadcastQueue);
|
|
4637
|
+
}
|
|
4638
|
+
async resolveAccess(user) {
|
|
4639
|
+
const canRead2 = await this.authorizeRead(user);
|
|
4640
|
+
if (!canRead2) return { canRead: false, canWrite: false };
|
|
4641
|
+
const canWrite = this.authorizeRead === this.authorizeWrite || await this.authorizeWrite(user);
|
|
4642
|
+
return { canRead: true, canWrite };
|
|
4643
|
+
}
|
|
4600
4644
|
// ==========================================================================
|
|
4601
4645
|
// Scheduling / recovery / pruning
|
|
4602
4646
|
// ==========================================================================
|