@hiai-gg/docsmint 0.3.5 → 0.3.6
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 +0 -18
- package/backend/src/lib/lifecycle-service.ts +334 -219
- package/dist/backend/index.js +232 -18
- package/dist/backend-launcher.d.ts +15 -0
- package/dist/backend-launcher.js +22 -0
- package/dist/frontend/extension.d.ts +2 -1
- package/dist/frontend/shared-document.js +89 -54
- package/dist/lifecycle-persistent.d.ts +31 -0
- package/dist/lifecycle-persistent.js +33 -0
- package/dist/server-only-browser-entry.d.ts +0 -0
- package/dist/server-only-browser-entry.js +3 -0
- package/dist/storage-quota.d.ts +36 -0
- package/dist/storage-quota.js +14 -1
- package/dist/types.d.ts +7 -0
- package/dist/workspace.d.ts +4 -1
- package/dist/workspace.js +29 -8
- package/package.json +2 -3
- package/packages/cli/src/index.ts +1 -1
- package/packages/db/src/index.ts +11 -2
- package/packages/db/src/schema.ts +30 -2
- package/packages/db/src/with-tenant.ts +38 -1
- package/packages/mcp-server/src/index.ts +1 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { t as e } from "./context-DbnW5yib.js";
|
|
2
|
-
import { $ as t, D as n, H as r, M as i, N as a, S as o, T as s, _t as c, dt as l, et as u, ft as d, j as f, k as p,
|
|
2
|
+
import { $ as t, D as n, H as r, M as i, N as a, S as o, T as s, _t as c, dt as l, et as u, ft as d, j as f, k as p, lt as m, tt as h } from "./client-C6QQGb0e.js";
|
|
3
3
|
//#region src/lib/components/editor/shared-document.ts
|
|
4
|
-
var
|
|
4
|
+
var g = /^\/api\/attachments\/[0-9a-f-]+\/raw$/i, _ = /* @__PURE__ */ new Set([
|
|
5
5
|
"http:",
|
|
6
6
|
"https:",
|
|
7
7
|
"mailto:"
|
|
8
8
|
]);
|
|
9
|
-
function
|
|
9
|
+
function v(e) {
|
|
10
10
|
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function y(e, t) {
|
|
13
13
|
switch (e.type) {
|
|
14
14
|
case "bold": return `<strong>${t}</strong>`;
|
|
15
15
|
case "italic": return `<em>${t}</em>`;
|
|
@@ -17,41 +17,41 @@ function v(e, t) {
|
|
|
17
17
|
case "strikethrough": return `<s>${t}</s>`;
|
|
18
18
|
case "underline": return `<u>${t}</u>`;
|
|
19
19
|
case "code": return `<code>${t}</code>`;
|
|
20
|
-
case "link": return `<a href="${
|
|
21
|
-
case "highlight": return `<mark style="background-color: ${
|
|
20
|
+
case "link": return `<a href="${v(b(e.attrs?.href ?? "#"))}" target="_blank" rel="noopener noreferrer">${t}</a>`;
|
|
21
|
+
case "highlight": return `<mark style="background-color: ${v(e.attrs?.color ?? "#fde68a")}">${t}</mark>`;
|
|
22
22
|
default: return t;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
function
|
|
25
|
+
function b(e) {
|
|
26
26
|
if (e.startsWith("#") || e.startsWith("/") || e.startsWith("./")) return e;
|
|
27
27
|
try {
|
|
28
28
|
let t = new URL(e);
|
|
29
|
-
return
|
|
29
|
+
return _.has(t.protocol) ? e : "#";
|
|
30
30
|
} catch {
|
|
31
31
|
return "#";
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
function
|
|
34
|
+
function x(e) {
|
|
35
35
|
let t = e?.textAlign;
|
|
36
36
|
return t !== "left" && t !== "center" && t !== "right" && t !== "justify" ? "" : ` style="text-align: ${t}"`;
|
|
37
37
|
}
|
|
38
|
-
function
|
|
38
|
+
function S(e) {
|
|
39
39
|
if (e.type === "text") {
|
|
40
|
-
let t =
|
|
41
|
-
for (let n of e.marks ?? []) t =
|
|
40
|
+
let t = v(e.text ?? "");
|
|
41
|
+
for (let n of e.marks ?? []) t = y(n, t);
|
|
42
42
|
return t;
|
|
43
43
|
}
|
|
44
|
-
return (e.content ?? []).map(
|
|
44
|
+
return (e.content ?? []).map(S).join("");
|
|
45
45
|
}
|
|
46
|
-
function
|
|
46
|
+
function C(e) {
|
|
47
47
|
let t = (e) => {
|
|
48
|
-
if (e.type === "text") return
|
|
49
|
-
let n =
|
|
48
|
+
if (e.type === "text") return S(e);
|
|
49
|
+
let n = x(e.attrs), r = (e.content ?? []).map(t).join("");
|
|
50
50
|
switch (e.type) {
|
|
51
51
|
case "paragraph": return `<p${n}>${r}</p>`;
|
|
52
52
|
case "heading": {
|
|
53
53
|
let t = Math.min(Math.max(Number(e.attrs?.level ?? 1), 1), 6);
|
|
54
|
-
return `<h${t}${n}>${(e.content ?? []).map(
|
|
54
|
+
return `<h${t}${n}>${(e.content ?? []).map(S).join("")}</h${t}>`;
|
|
55
55
|
}
|
|
56
56
|
case "bulletList": return `<ul${n}>${r}</ul>`;
|
|
57
57
|
case "orderedList": {
|
|
@@ -71,34 +71,34 @@ function S(e) {
|
|
|
71
71
|
case "tableCell": return `<td${n}>${r}</td>`;
|
|
72
72
|
case "codeBlock": {
|
|
73
73
|
let t = e.attrs?.language ?? "";
|
|
74
|
-
return `<pre><code${t ? ` class="language-${
|
|
74
|
+
return `<pre><code${t ? ` class="language-${v(t)}"` : ""}>${r}</code></pre>`;
|
|
75
75
|
}
|
|
76
76
|
case "horizontalRule": return "<hr />";
|
|
77
77
|
case "hardBreak": return "<br />";
|
|
78
78
|
case "image": {
|
|
79
79
|
let t = e.attrs?.src ?? "", n = e.attrs?.alt ?? "", r = Number(e.attrs?.width), i = Number(e.attrs?.height), a = `${Number.isFinite(r) && r > 0 ? ` width="${Math.round(r)}"` : ""}${Number.isFinite(i) && i > 0 ? ` height="${Math.round(i)}"` : ""}`;
|
|
80
|
-
return
|
|
80
|
+
return g.test(t) ? `<img data-shared-attachment-src="${v(t)}" alt="${v(n)}"${a} />` : `<img src="${v(t)}" alt="${v(n)}"${a} />`;
|
|
81
81
|
}
|
|
82
82
|
default: return r;
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
return (e.content ?? []).map(t).join("");
|
|
86
86
|
}
|
|
87
|
-
function
|
|
87
|
+
function w(e) {
|
|
88
88
|
return e.replace(/<li>(\s*<input\b[^>]*type=["']checkbox["'][^>]*>)/gi, "<li class=\"task-list-item\">$1");
|
|
89
89
|
}
|
|
90
|
-
function
|
|
90
|
+
function T(e, t = "") {
|
|
91
91
|
return {
|
|
92
92
|
"x-share-token": e,
|
|
93
93
|
...t ? { "x-share-password": t } : {}
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
-
async function
|
|
96
|
+
async function E(e, t, n = "") {
|
|
97
97
|
let r = [], i = e.querySelectorAll("img[data-shared-attachment-src]");
|
|
98
98
|
return await Promise.all(Array.from(i, async (e) => {
|
|
99
99
|
let i = e.dataset.sharedAttachmentSrc;
|
|
100
100
|
if (i) try {
|
|
101
|
-
let a = await fetch(i, { headers:
|
|
101
|
+
let a = await fetch(i, { headers: T(t, n) });
|
|
102
102
|
if (!a.ok) {
|
|
103
103
|
e.dataset.sharedAttachmentError = String(a.status);
|
|
104
104
|
return;
|
|
@@ -110,7 +110,7 @@ async function T(e, t, n = "") {
|
|
|
110
110
|
}
|
|
111
111
|
})), r;
|
|
112
112
|
}
|
|
113
|
-
async function
|
|
113
|
+
async function D(e) {
|
|
114
114
|
let t = Array.from(e.querySelectorAll("img"));
|
|
115
115
|
await Promise.all(t.map(async (e) => {
|
|
116
116
|
if (!e.complete) {
|
|
@@ -125,69 +125,104 @@ async function E(e) {
|
|
|
125
125
|
}));
|
|
126
126
|
}
|
|
127
127
|
//#endregion
|
|
128
|
+
//#region src/lib/extensions/shared-document-context.ts
|
|
129
|
+
var O = /* @__PURE__ */ new Set([
|
|
130
|
+
"shareToken",
|
|
131
|
+
"password",
|
|
132
|
+
"passwordHash",
|
|
133
|
+
"workspaceAssertion",
|
|
134
|
+
"authorization",
|
|
135
|
+
"cookie",
|
|
136
|
+
"signingSecret"
|
|
137
|
+
]);
|
|
138
|
+
function k(e) {
|
|
139
|
+
if (!e.id.trim() || !e.expiresAt.trim()) throw TypeError("Shared extension capability must include a non-empty id and expiry");
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
id: e.id,
|
|
142
|
+
expiresAt: e.expiresAt
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function A(e) {
|
|
146
|
+
for (let t of Object.keys(e)) if (O.has(t)) throw TypeError(`Sensitive ${t} must not be provided to a shared extension`);
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
documentId: e.documentId,
|
|
149
|
+
title: e.title,
|
|
150
|
+
content: e.content,
|
|
151
|
+
...e.contentJson === void 0 ? {} : { contentJson: e.contentJson },
|
|
152
|
+
role: e.role,
|
|
153
|
+
capability: k(e.capability),
|
|
154
|
+
permissions: Object.freeze({
|
|
155
|
+
read: !0,
|
|
156
|
+
annotate: e.permissions.annotate,
|
|
157
|
+
edit: e.permissions.edit,
|
|
158
|
+
export: e.permissions.export
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
128
163
|
//#region src/lib/hosts/DocsmintSharedDocumentHost.svelte
|
|
129
|
-
var
|
|
130
|
-
function
|
|
131
|
-
d(
|
|
132
|
-
let
|
|
133
|
-
function
|
|
134
|
-
return e.permission === "annotate" ?
|
|
164
|
+
var j = a("<div data-docsmint-shared-document-host=\"\"><div data-extension-zone=\"shared-header-actions\"></div> <!> <div data-extension-zone=\"shared-document-tabs\"></div> <div data-extension-zone=\"shared-document-notes\"></div> <div data-extension-zone=\"shared-document-editor\"><!></div></div>");
|
|
165
|
+
function M(a, g) {
|
|
166
|
+
d(g, !0);
|
|
167
|
+
let _ = e(), v = m(() => A(g.context));
|
|
168
|
+
function y(e) {
|
|
169
|
+
return e.permission === "annotate" ? r(v).permissions.annotate : r(v).permissions.edit;
|
|
135
170
|
}
|
|
136
|
-
function
|
|
171
|
+
function b(e) {
|
|
137
172
|
let t = /* @__PURE__ */ new Set();
|
|
138
173
|
return e.filter((e) => {
|
|
139
|
-
if (t.has(e.id) || !
|
|
174
|
+
if (t.has(e.id) || !y(e)) return !1;
|
|
140
175
|
t.add(e.id);
|
|
141
176
|
try {
|
|
142
|
-
return e.visible?.(
|
|
177
|
+
return e.visible?.(r(v)) ?? !0;
|
|
143
178
|
} catch {
|
|
144
179
|
return !1;
|
|
145
180
|
}
|
|
146
181
|
}).sort((e, t) => (e.order ?? 0) - (t.order ?? 0) || e.id.localeCompare(t.id));
|
|
147
182
|
}
|
|
148
|
-
var
|
|
149
|
-
s(
|
|
183
|
+
var x = j(), S = t(x);
|
|
184
|
+
s(S, 21, () => b(_.sharedDocumentHeaderActions), (e) => e.id, (e, t) => {
|
|
150
185
|
var n = i();
|
|
151
186
|
o(u(n), () => r(t).component, (e, t) => {
|
|
152
187
|
t(e, { get context() {
|
|
153
|
-
return
|
|
188
|
+
return r(v);
|
|
154
189
|
} });
|
|
155
190
|
}), f(e, n);
|
|
156
|
-
}), c(
|
|
157
|
-
var
|
|
158
|
-
p(
|
|
159
|
-
var
|
|
160
|
-
s(
|
|
191
|
+
}), c(S);
|
|
192
|
+
var C = h(S, 2);
|
|
193
|
+
p(C, () => g.children);
|
|
194
|
+
var w = h(C, 2);
|
|
195
|
+
s(w, 21, () => b(_.sharedDocumentTabs), (e) => e.id, (e, t) => {
|
|
161
196
|
var n = i();
|
|
162
197
|
o(u(n), () => r(t).component, (e, t) => {
|
|
163
198
|
t(e, { get context() {
|
|
164
|
-
return
|
|
199
|
+
return r(v);
|
|
165
200
|
} });
|
|
166
201
|
}), f(e, n);
|
|
167
|
-
}), c(
|
|
168
|
-
var
|
|
169
|
-
s(
|
|
202
|
+
}), c(w);
|
|
203
|
+
var T = h(w, 2);
|
|
204
|
+
s(T, 21, () => b(_.sharedDocumentNotesModes), (e) => e.id, (e, t) => {
|
|
170
205
|
var n = i();
|
|
171
206
|
o(u(n), () => r(t).component, (e, t) => {
|
|
172
207
|
t(e, { get context() {
|
|
173
|
-
return
|
|
208
|
+
return r(v);
|
|
174
209
|
} });
|
|
175
210
|
}), f(e, n);
|
|
176
|
-
}), c(
|
|
177
|
-
var
|
|
211
|
+
}), c(T);
|
|
212
|
+
var E = h(T, 2), D = t(E), O = (e) => {
|
|
178
213
|
var t = i();
|
|
179
|
-
s(u(t), 17, () =>
|
|
214
|
+
s(u(t), 17, () => b(_.sharedDocumentEditorModes), (e) => e.id, (e, t) => {
|
|
180
215
|
var n = i();
|
|
181
216
|
o(u(n), () => r(t).component, (e, t) => {
|
|
182
217
|
t(e, { get context() {
|
|
183
|
-
return
|
|
218
|
+
return r(v);
|
|
184
219
|
} });
|
|
185
220
|
}), f(e, n);
|
|
186
221
|
}), f(e, t);
|
|
187
222
|
};
|
|
188
|
-
n(
|
|
189
|
-
|
|
190
|
-
}), c(
|
|
223
|
+
n(D, (e) => {
|
|
224
|
+
r(v).permissions.edit && e(O);
|
|
225
|
+
}), c(E), c(x), f(a, x), l();
|
|
191
226
|
}
|
|
192
227
|
//#endregion
|
|
193
|
-
export {
|
|
228
|
+
export { M as DocsmintSharedDocumentHost, E as hydrateSharedAttachmentImages, w as markMarkdownTaskItems, C as renderSharedDocument, T as sharedAttachmentHeaders, D as waitForSharedDocumentImages };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type AssertPurgeAllowed, type LifecycleHostStep, type UserDataLifecycle, type UserDataLifecycleAdapter } from "./lifecycle";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal transaction boundary required by the durable lifecycle runtime.
|
|
4
|
+
* Hosts supply a request/RLS-scoped executor; SDK code never imports a global
|
|
5
|
+
* database singleton or bypasses host tenant policy.
|
|
6
|
+
*/
|
|
7
|
+
export type LifecycleScopedDatabaseExecutor = <T>(context: Readonly<{
|
|
8
|
+
actorUserId: string;
|
|
9
|
+
requestId: string;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}>, operation: () => Promise<T>) => Promise<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Public persistent lifecycle composition contract. The OSS persistence
|
|
14
|
+
* saga is supplied by the backend runtime; SaaS hosts attach their own steps
|
|
15
|
+
* and RLS executor without private imports.
|
|
16
|
+
*/
|
|
17
|
+
export type LifecycleRuntimeAdapters = Readonly<{
|
|
18
|
+
database: LifecycleScopedDatabaseExecutor;
|
|
19
|
+
adapter: UserDataLifecycleAdapter;
|
|
20
|
+
}>;
|
|
21
|
+
export type PersistentLifecycleRuntimeOptions = Readonly<{
|
|
22
|
+
runtime: LifecycleRuntimeAdapters;
|
|
23
|
+
assertPurgeAllowed: AssertPurgeAllowed;
|
|
24
|
+
hostSteps?: readonly LifecycleHostStep[];
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Creates an immutable, transaction-scoped public lifecycle facade.
|
|
28
|
+
* The database executor is invoked for every OSS adapter operation so hosts
|
|
29
|
+
* can set RLS GUCs and reject a missing scope before any mutation occurs.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createPersistentLifecycleRuntime(options: PersistentLifecycleRuntimeOptions): UserDataLifecycle;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createUserDataLifecycle, orderLifecycleHostSteps, } from "./lifecycle";
|
|
2
|
+
function immutableContext(context) {
|
|
3
|
+
return Object.freeze({ ...context });
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Creates an immutable, transaction-scoped public lifecycle facade.
|
|
7
|
+
* The database executor is invoked for every OSS adapter operation so hosts
|
|
8
|
+
* can set RLS GUCs and reject a missing scope before any mutation occurs.
|
|
9
|
+
*/
|
|
10
|
+
export function createPersistentLifecycleRuntime(options) {
|
|
11
|
+
const hostSteps = orderLifecycleHostSteps(options.hostSteps ?? []);
|
|
12
|
+
const lifecycle = createUserDataLifecycle({
|
|
13
|
+
async *exportUserData(context) {
|
|
14
|
+
const immutable = immutableContext(context);
|
|
15
|
+
const records = await options.runtime.database(immutable, async () => {
|
|
16
|
+
const result = [];
|
|
17
|
+
for await (const record of options.runtime.adapter.exportUserData(immutable))
|
|
18
|
+
result.push(record);
|
|
19
|
+
return result;
|
|
20
|
+
});
|
|
21
|
+
for (const record of records)
|
|
22
|
+
yield record;
|
|
23
|
+
},
|
|
24
|
+
async purgeUserData(context, gate) {
|
|
25
|
+
const immutable = immutableContext(context);
|
|
26
|
+
return options.runtime.database(immutable, () => options.runtime.adapter.purgeUserData(immutable, gate));
|
|
27
|
+
},
|
|
28
|
+
}, async (context) => options.assertPurgeAllowed(immutableContext(context)));
|
|
29
|
+
// Validate host-step ordering eagerly. The OSS adapter owns invocation; the
|
|
30
|
+
// public factory records the accepted contract without inventing SaaS data.
|
|
31
|
+
void hostSteps;
|
|
32
|
+
return lifecycle;
|
|
33
|
+
}
|
|
File without changes
|
package/dist/storage-quota.d.ts
CHANGED
|
@@ -46,6 +46,42 @@ export type StorageQuotaService = Readonly<{
|
|
|
46
46
|
commit(request: StorageQuotaCommitRequest): Promise<StorageQuotaCommitResult>;
|
|
47
47
|
release(request: StorageQuotaReleaseRequest): Promise<StorageQuotaReleaseResult>;
|
|
48
48
|
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Verified server-side identity for an attachment write. It is intentionally
|
|
51
|
+
* richer than the generic quota adapter: attachment storage must be charged
|
|
52
|
+
* to one workspace, actor and immutable object key before a presign is issued.
|
|
53
|
+
*/
|
|
54
|
+
export type AttachmentStorageQuotaContext = Readonly<{
|
|
55
|
+
workspaceId: string;
|
|
56
|
+
actorUserId: string;
|
|
57
|
+
documentId: string;
|
|
58
|
+
storageKey: string;
|
|
59
|
+
proposedSize: number;
|
|
60
|
+
requestId: string;
|
|
61
|
+
idempotencyKey: string;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}>;
|
|
64
|
+
export type AttachmentStorageQuotaFinalization = Readonly<{
|
|
65
|
+
reservationId: string;
|
|
66
|
+
actualSize: number;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* OSS invokes this only after it has verified tenant context and document
|
|
70
|
+
* access. A host provider is never exposed to HTTP or browser code.
|
|
71
|
+
*/
|
|
72
|
+
export type AttachmentStorageQuotaAdmission = Readonly<{
|
|
73
|
+
reserve(context: AttachmentStorageQuotaContext): Promise<Readonly<{
|
|
74
|
+
id: string;
|
|
75
|
+
}>>;
|
|
76
|
+
finalize(context: AttachmentStorageQuotaContext, finalization: AttachmentStorageQuotaFinalization): Promise<void>;
|
|
77
|
+
releaseReservation(context: AttachmentStorageQuotaContext, reservationId: string): Promise<void>;
|
|
78
|
+
releaseCommitted(context: AttachmentStorageQuotaContext): Promise<void>;
|
|
79
|
+
}>;
|
|
80
|
+
export declare class MissingAttachmentStorageQuotaAdmissionError extends Error {
|
|
81
|
+
readonly code: "ATTACHMENT_STORAGE_QUOTA_ADMISSION_MISSING";
|
|
82
|
+
constructor();
|
|
83
|
+
}
|
|
84
|
+
export declare function requireAttachmentStorageQuotaAdmission(admission: AttachmentStorageQuotaAdmission | undefined): AttachmentStorageQuotaAdmission;
|
|
49
85
|
export declare class StorageQuotaExceededError extends Error {
|
|
50
86
|
readonly code: "STORAGE_QUOTA_EXCEEDED";
|
|
51
87
|
readonly usageBytes: number;
|
package/dist/storage-quota.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
/** Server-only, persistence-agnostic storage quota contract. */
|
|
2
|
+
export class MissingAttachmentStorageQuotaAdmissionError extends Error {
|
|
3
|
+
code = "ATTACHMENT_STORAGE_QUOTA_ADMISSION_MISSING";
|
|
4
|
+
constructor() {
|
|
5
|
+
super("Attachment storage quota admission is required when workspace tenancy is enabled");
|
|
6
|
+
this.name = "MissingAttachmentStorageQuotaAdmissionError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function requireAttachmentStorageQuotaAdmission(admission) {
|
|
10
|
+
if (!admission)
|
|
11
|
+
throw new MissingAttachmentStorageQuotaAdmissionError();
|
|
12
|
+
return admission;
|
|
13
|
+
}
|
|
2
14
|
export class StorageQuotaExceededError extends Error {
|
|
3
15
|
code = "STORAGE_QUOTA_EXCEEDED";
|
|
4
16
|
usageBytes;
|
|
@@ -71,7 +83,8 @@ export function createStorageQuotaService(adapter) {
|
|
|
71
83
|
assertReservationId(request.reservationId);
|
|
72
84
|
assertPositiveBytes(request.actualBytes, "actualBytes");
|
|
73
85
|
const result = await adapter.commit(Object.freeze({ ...request }));
|
|
74
|
-
if (result.status !== "committed" &&
|
|
86
|
+
if (result.status !== "committed" &&
|
|
87
|
+
result.status !== "already_committed") {
|
|
75
88
|
throw new TypeError("Storage quota adapter returned an invalid commit status");
|
|
76
89
|
}
|
|
77
90
|
return Object.freeze({ ...result });
|
package/dist/types.d.ts
CHANGED
|
@@ -294,11 +294,18 @@ export interface DocsAttachmentPresignInput {
|
|
|
294
294
|
export interface DocsAttachmentPresignResponse {
|
|
295
295
|
url: string;
|
|
296
296
|
key: string;
|
|
297
|
+
/**
|
|
298
|
+
* Opaque server-issued reservation. Present only when workspace storage
|
|
299
|
+
* enforcement is enabled and required verbatim by confirmAttachment.
|
|
300
|
+
*/
|
|
301
|
+
quotaReservationId?: string;
|
|
297
302
|
maxSize: number;
|
|
298
303
|
expiresIn: number;
|
|
299
304
|
}
|
|
300
305
|
export interface DocsAttachmentConfirmInput extends DocsAttachmentPresignInput {
|
|
301
306
|
key: string;
|
|
307
|
+
/** The opaque reservation returned by presignAttachment when present. */
|
|
308
|
+
quotaReservationId?: string;
|
|
302
309
|
}
|
|
303
310
|
export type DocsApiKeyScope = "global" | `category:${string}:${"read" | "edit" | "write"}`;
|
|
304
311
|
export interface DocsApiKeyCreated {
|
package/dist/workspace.d.ts
CHANGED
|
@@ -15,7 +15,10 @@ export type WorkspaceAssertionOptions = Readonly<{
|
|
|
15
15
|
issuer: string;
|
|
16
16
|
nowSeconds?: number;
|
|
17
17
|
clockSkewSeconds?: number;
|
|
18
|
-
|
|
18
|
+
/** @deprecated Assertion lifetime is fixed at 60 seconds. */
|
|
19
|
+
maxTtlSeconds?: never;
|
|
19
20
|
}>;
|
|
21
|
+
export declare const DOCSMINT_WORKSPACE_ASSERTION_TTL_SECONDS = 60;
|
|
22
|
+
export declare const DOCSMINT_WORKSPACE_ASSERTION_CLOCK_SKEW_SECONDS = 5;
|
|
20
23
|
export declare function createDocsmintWorkspaceAssertion(context: DocsmintWorkspaceContext, secret: string): Promise<string>;
|
|
21
24
|
export declare function verifyDocsmintWorkspaceAssertion(assertion: string, options: WorkspaceAssertionOptions): Promise<DocsmintWorkspaceContext>;
|
package/dist/workspace.js
CHANGED
|
@@ -4,17 +4,22 @@ export const DOCSMINT_WORKSPACE_CONTEXT_HEADER = "x-docsmint-workspace-context";
|
|
|
4
4
|
export const EXTERNAL_TENANT_CONTEXT_HEADER = "x-hiai-tenant-context";
|
|
5
5
|
const encoder = new TextEncoder();
|
|
6
6
|
const decoder = new TextDecoder();
|
|
7
|
-
const
|
|
7
|
+
export const DOCSMINT_WORKSPACE_ASSERTION_TTL_SECONDS = 60;
|
|
8
|
+
export const DOCSMINT_WORKSPACE_ASSERTION_CLOCK_SKEW_SECONDS = 5;
|
|
8
9
|
function toBase64Url(bytes) {
|
|
9
10
|
let binary = "";
|
|
10
11
|
for (const byte of bytes)
|
|
11
12
|
binary += String.fromCharCode(byte);
|
|
12
|
-
return btoa(binary)
|
|
13
|
+
return btoa(binary)
|
|
14
|
+
.replaceAll("+", "-")
|
|
15
|
+
.replaceAll("/", "_")
|
|
16
|
+
.replace(/=+$/, "");
|
|
13
17
|
}
|
|
14
18
|
function fromBase64Url(value) {
|
|
15
19
|
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
16
20
|
throw new Error("Invalid base64url");
|
|
17
|
-
const padded = value.replaceAll("-", "+").replaceAll("_", "/") +
|
|
21
|
+
const padded = value.replaceAll("-", "+").replaceAll("_", "/") +
|
|
22
|
+
"=".repeat((4 - (value.length % 4)) % 4);
|
|
18
23
|
const binary = atob(padded);
|
|
19
24
|
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
20
25
|
}
|
|
@@ -26,13 +31,20 @@ function assertContext(value) {
|
|
|
26
31
|
if (!value || typeof value !== "object")
|
|
27
32
|
throw new Error("Invalid workspace assertion payload");
|
|
28
33
|
const context = value;
|
|
29
|
-
if (typeof context.actorUserId !== "string" ||
|
|
34
|
+
if (typeof context.actorUserId !== "string" ||
|
|
35
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(context.actorUserId))
|
|
30
36
|
throw new Error("Invalid actorUserId");
|
|
31
|
-
if (typeof context.workspaceId !== "string" ||
|
|
37
|
+
if (typeof context.workspaceId !== "string" ||
|
|
38
|
+
!context.workspaceId.trim() ||
|
|
39
|
+
context.workspaceId.trim() !== context.workspaceId ||
|
|
40
|
+
context.workspaceId.length > 128)
|
|
32
41
|
throw new Error("Invalid workspaceId");
|
|
33
42
|
if (!["owner", "admin", "editor", "viewer"].includes(context.actorRole))
|
|
34
43
|
throw new Error("Invalid actorRole");
|
|
35
|
-
if (!Number.isFinite(context.issuedAt) ||
|
|
44
|
+
if (!Number.isFinite(context.issuedAt) ||
|
|
45
|
+
!Number.isFinite(context.expiresAt) ||
|
|
46
|
+
typeof context.issuer !== "string" ||
|
|
47
|
+
!context.issuer)
|
|
36
48
|
throw new Error("Invalid workspace assertion timestamps or issuer");
|
|
37
49
|
}
|
|
38
50
|
export async function createDocsmintWorkspaceAssertion(context, secret) {
|
|
@@ -59,8 +71,17 @@ export async function verifyDocsmintWorkspaceAssertion(assertion, options) {
|
|
|
59
71
|
if (context.issuer !== options.issuer)
|
|
60
72
|
throw new Error("Invalid workspace assertion issuer");
|
|
61
73
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
62
|
-
const skew = options.clockSkewSeconds ??
|
|
63
|
-
if (
|
|
74
|
+
const skew = options.clockSkewSeconds ?? DOCSMINT_WORKSPACE_ASSERTION_CLOCK_SKEW_SECONDS;
|
|
75
|
+
if (!Number.isSafeInteger(skew) ||
|
|
76
|
+
skew < 0 ||
|
|
77
|
+
skew > DOCSMINT_WORKSPACE_ASSERTION_CLOCK_SKEW_SECONDS) {
|
|
78
|
+
throw new Error("Invalid workspace assertion clock skew");
|
|
79
|
+
}
|
|
80
|
+
if (context.issuedAt > now + skew ||
|
|
81
|
+
context.expiresAt <= now - skew ||
|
|
82
|
+
context.expiresAt <= context.issuedAt ||
|
|
83
|
+
context.expiresAt - context.issuedAt >
|
|
84
|
+
DOCSMINT_WORKSPACE_ASSERTION_TTL_SECONDS)
|
|
64
85
|
throw new Error("Invalid workspace assertion lifetime");
|
|
65
86
|
return Object.freeze({ ...context });
|
|
66
87
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiai-gg/docsmint",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"browser": {
|
|
6
6
|
"./dist/backend-launcher.js": false,
|
|
7
7
|
"./dist/lifecycle.js": false,
|
|
8
|
+
"./dist/lifecycle-persistent.js": false,
|
|
8
9
|
"./dist/storage-quota.js": false,
|
|
9
10
|
"./dist/workspace.js": false
|
|
10
11
|
},
|
|
@@ -166,12 +167,10 @@
|
|
|
166
167
|
"packages/db/src/client.ts",
|
|
167
168
|
"packages/db/src/schema.ts",
|
|
168
169
|
"packages/db/src/with-tenant.ts",
|
|
169
|
-
"packages/cli/src/index.ts",
|
|
170
170
|
"packages/cli/src/client.ts",
|
|
171
171
|
"packages/cli/src/config.ts",
|
|
172
172
|
"packages/cli/src/format.ts",
|
|
173
173
|
"packages/cli/src/commands",
|
|
174
|
-
"packages/mcp-server/src/index.ts",
|
|
175
174
|
"packages/mcp-server/src/client.ts",
|
|
176
175
|
"packages/mcp-server/src/types.ts",
|
|
177
176
|
"packages/mcp-server/src/tools",
|
|
@@ -24,7 +24,7 @@ import { registerSearch } from "./commands/search.js";
|
|
|
24
24
|
import { registerSnapshot } from "./commands/snapshot.js";
|
|
25
25
|
import { registerUpdate } from "./commands/update.js";
|
|
26
26
|
|
|
27
|
-
const VERSION = "0.3.
|
|
27
|
+
const VERSION = "0.3.6";
|
|
28
28
|
|
|
29
29
|
const program = new Command();
|
|
30
30
|
program
|
package/packages/db/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export * from "./schema";
|
|
2
2
|
export { db, client } from "./client";
|
|
3
3
|
export type { Database } from "./client";
|
|
4
|
-
export {
|
|
5
|
-
|
|
4
|
+
export {
|
|
5
|
+
withTenant,
|
|
6
|
+
adminTenantContext,
|
|
7
|
+
shareGuestTenantContext,
|
|
8
|
+
createActorScopedTransactionExecutor,
|
|
9
|
+
} from "./with-tenant";
|
|
10
|
+
export type {
|
|
11
|
+
ActorScopedTransactionExecutor,
|
|
12
|
+
TenantContext,
|
|
13
|
+
TenantTransaction,
|
|
14
|
+
} from "./with-tenant";
|
|
@@ -698,7 +698,8 @@ export const lifecycleOperations = pgTable(
|
|
|
698
698
|
"lifecycle_operations",
|
|
699
699
|
{
|
|
700
700
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
701
|
-
|
|
701
|
+
actorUserId: uuid("actor_user_id").references(() => users.id, { onDelete: "set null" }),
|
|
702
|
+
actorSubjectHash: text("actor_subject_hash").notNull(),
|
|
702
703
|
idempotencyKey: text("idempotency_key").notNull(),
|
|
703
704
|
operationKind: lifecycleOperationKindEnum("operation_kind").notNull(),
|
|
704
705
|
status: lifecycleOperationStatusEnum("status").notNull().default("pending"),
|
|
@@ -717,7 +718,34 @@ export const lifecycleOperations = pgTable(
|
|
|
717
718
|
uniqueIndex("lifecycle_operations_actor_idempotency_idx").on(table.actorUserId, table.idempotencyKey),
|
|
718
719
|
index("lifecycle_operations_status_lease_idx").on(table.status, table.leaseExpiresAt),
|
|
719
720
|
index("lifecycle_operations_actor_idx").on(table.actorUserId),
|
|
720
|
-
|
|
721
|
+
index("lifecycle_operations_retryable_idx").on(table.status).where(sql`${table.status} = 'retryable'`),
|
|
722
|
+
check("lifecycle_operations_actor_subject_hash", sql`${table.actorSubjectHash} ~ '^[a-f0-9]{64}$'`),
|
|
723
|
+
],
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
// Atomically committed alongside a document and its initial version when an
|
|
727
|
+
// authenticated client supplies Idempotency-Key on POST /api/documents.
|
|
728
|
+
export const documentCreateOperations = pgTable(
|
|
729
|
+
"document_create_operations",
|
|
730
|
+
{
|
|
731
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
732
|
+
workspaceId: text("workspace_id").notNull(),
|
|
733
|
+
actorUserId: uuid("actor_user_id")
|
|
734
|
+
.notNull()
|
|
735
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
736
|
+
idempotencyKey: text("idempotency_key").notNull(),
|
|
737
|
+
documentId: uuid("document_id")
|
|
738
|
+
.notNull()
|
|
739
|
+
.references(() => documents.id, { onDelete: "cascade" }),
|
|
740
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
741
|
+
},
|
|
742
|
+
(table) => [
|
|
743
|
+
uniqueIndex("document_create_operations_workspace_actor_key_idx").on(
|
|
744
|
+
table.workspaceId,
|
|
745
|
+
table.actorUserId,
|
|
746
|
+
table.idempotencyKey,
|
|
747
|
+
),
|
|
748
|
+
uniqueIndex("document_create_operations_document_idx").on(table.documentId),
|
|
721
749
|
],
|
|
722
750
|
);
|
|
723
751
|
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
|
-
import { db } from "./client";
|
|
2
|
+
import { db, type Database } from "./client";
|
|
3
|
+
|
|
4
|
+
/** A transaction carrying the same query surface as the tenant-scoped DB. */
|
|
5
|
+
export type TenantTransaction = Parameters<
|
|
6
|
+
Parameters<typeof db.transaction>[0]
|
|
7
|
+
>[0];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Server-side adapter for packages that must run short actor-scoped database
|
|
11
|
+
* transactions without importing this module's process-global client.
|
|
12
|
+
*/
|
|
13
|
+
export interface ActorScopedTransactionExecutor {
|
|
14
|
+
withActorTransaction<T>(
|
|
15
|
+
actorUserId: string,
|
|
16
|
+
operation: (tx: TenantTransaction) => Promise<T>,
|
|
17
|
+
): Promise<T>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createActorScopedTransactionExecutor(
|
|
21
|
+
database: Database,
|
|
22
|
+
): ActorScopedTransactionExecutor {
|
|
23
|
+
return {
|
|
24
|
+
withActorTransaction(actorUserId, operation) {
|
|
25
|
+
return database.transaction(async (tx) => {
|
|
26
|
+
await tx.execute(
|
|
27
|
+
sql`SELECT set_config('app.current_user_id', ${actorUserId}, true)`,
|
|
28
|
+
);
|
|
29
|
+
await tx.execute(
|
|
30
|
+
sql`SELECT set_config('app.current_user_role', 'user', true)`,
|
|
31
|
+
);
|
|
32
|
+
await tx.execute(
|
|
33
|
+
sql`SELECT set_config('app.current_workspace_id', '', true)`,
|
|
34
|
+
);
|
|
35
|
+
return operation(tx);
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
3
40
|
|
|
4
41
|
export interface TenantContext {
|
|
5
42
|
userId: string;
|