@hollware/server 1.0.0-rc.1 → 1.0.0-rc.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/dist/{ceremonies-CI3DkOEL.mjs → ceremonies-zTn1U6BD.mjs} +1 -1
- package/dist/index.d.mts +73 -2
- package/dist/index.mjs +431 -76
- package/dist/{store-DjA9s95l.mjs → store-ApxKV0Sc.mjs} +34 -0
- package/migrations/0017_assets.sql +42 -0
- package/migrations/meta/0017_snapshot.json +2993 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as revisionTags, C as entryEmbeddings, D as publications, E as outbox, F as users, I as ROLE_SCOPES, L as effectiveScopes, M as schedules, N as sites, O as reviewComments, P as tokens, R as isRole, S as entries, T as members, a as dropExpired, f as membershipsOf, g as removePasskey, j as revisions, k as reviews, n as createInvitation, o as endSession, p as passkeySummaries, s as endSessions, u as findSession, w as invitations, x as deliveries } from "./store-
|
|
2
|
-
import { CHUNKER_VERSION, ConfigError, EVENT_ID_HEADER, HollwareError, INDEXNOW_KEY, OPERATIONS, OPERATION_AUDIT_ACTIONS, PREVIEW_QUERY_PARAM, SCOPES, SIGNATURE_HEADER, TIMESTAMP_HEADER, canSignPreviews, canonicalJson, chunksOf, compileSites, definitionsFromConfig, describeBuiltInContracts, describeCatalog, describeResolvedSite, describeSite, generatePersonalToken, generateReadToken, getEntry, getTranslations, hashPersonalToken, isAuditAction, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, makePreviewToken, newId, processMarkdown, publish, renderHtml, resolveConfig, runContracts, search, signWebhook, siteDefinitionData, sizeLimitViolation, tagsOfChange, tagsOfSiteChange, uniqueSlugViolation, uniqueTranslationViolation, verifyPreviewToken } from "@hollware/core";
|
|
1
|
+
import { A as revisionTags, C as entryEmbeddings, D as publications, E as outbox, F as users, I as ROLE_SCOPES, L as effectiveScopes, M as schedules, N as sites, O as reviewComments, P as tokens, R as isRole, S as entries, T as members, a as dropExpired, f as membershipsOf, g as removePasskey, j as revisions, k as reviews, n as createInvitation, o as endSession, p as passkeySummaries, s as endSessions, u as findSession, w as invitations, x as deliveries } from "./store-ApxKV0Sc.mjs";
|
|
2
|
+
import { CHUNKER_VERSION, ConfigError, EVENT_ID_HEADER, HollwareError, INDEXNOW_KEY, MAX_ASSET_BYTES, OPERATIONS, OPERATION_AUDIT_ACTIONS, PREVIEW_QUERY_PARAM, SCOPES, SIGNATURE_HEADER, TIMESTAMP_HEADER, assetIdOf, canSignPreviews, canonicalJson, checkSvg, chunksOf, compileSites, conversionOf, definitionsFromConfig, describeBuiltInContracts, describeCatalog, describeResolvedSite, describeSite, generatePersonalToken, generateReadToken, getEntry, getTranslations, hashPersonalToken, isAuditAction, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, makePreviewToken, newId, processMarkdown, publish, renderHtml, resolveAssetReferences, resolveConfig, runContracts, search, signWebhook, siteDefinitionData, sizeLimitViolation, sniffImage, tagsOfChange, tagsOfSiteChange, uniqueSlugViolation, uniqueTranslationViolation, verifyPreviewToken } from "@hollware/core";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { drizzle } from "drizzle-orm/d1";
|
|
5
5
|
import { and, asc, desc, eq, getTableColumns, gt, inArray, isNotNull, isNull, lte, ne, or, sql } from "drizzle-orm";
|
|
@@ -10,7 +10,7 @@ import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, McpServer, accepted
|
|
|
10
10
|
import * as z from "zod";
|
|
11
11
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
12
12
|
//#region package.json
|
|
13
|
-
var version = "1.0.0-rc.
|
|
13
|
+
var version = "1.0.0-rc.2";
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/auth/responses.ts
|
|
16
16
|
/** The challenge of every 401 and 403 (RFC 6750). */
|
|
@@ -257,6 +257,237 @@ async function checkPersonalToken(db, token, options) {
|
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
//#endregion
|
|
260
|
+
//#region src/ops/errors.ts
|
|
261
|
+
/**
|
|
262
|
+
* The error of something that must never happen: a bug of Hollware, not a mistake of the caller.
|
|
263
|
+
* Like the `internal` error of the adapters, its message says nothing about the cause, which goes
|
|
264
|
+
* to the log through `cause`, and its fix names the request id the operator finds the log line by.
|
|
265
|
+
*/
|
|
266
|
+
function internalError$1(context, cause) {
|
|
267
|
+
return new HollwareError("internal", "The operation failed on the server.", `Try again. If it keeps failing, give the site operator this request id: ${context.requestId}.`, { cause: new Error(cause) });
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The error for an answer of the store that the call it came from can never get: an entry that
|
|
271
|
+
* is created cannot conflict, a publish has no slug to take. It is a bug, not an outcome of the
|
|
272
|
+
* operation, so it is not one of the outcomes of its spec.
|
|
273
|
+
*/
|
|
274
|
+
function unexpectedAnswer(context, call, answer) {
|
|
275
|
+
return internalError$1(context, `The store answered ${answer.outcome} to ${call}, which it never does.`);
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/ops/actor.ts
|
|
279
|
+
/**
|
|
280
|
+
* Who a write is made by: the person behind the token of the call, with what its client said
|
|
281
|
+
* about itself. A personal token writes as itself (ADR-019); an OAuth access token writes as the
|
|
282
|
+
* client the person allowed, whose id goes with what the client said (ADR-026). The adapters check
|
|
283
|
+
* the scopes of the operation before any handler runs and never route a write for the RPC
|
|
284
|
+
* binding, so reaching one with it is a bug, answered as `internal` before anything is read or
|
|
285
|
+
* written.
|
|
286
|
+
*/
|
|
287
|
+
function actorOf$1(context) {
|
|
288
|
+
const { principal } = context;
|
|
289
|
+
if (principal.kind === "token") return {
|
|
290
|
+
kind: "token",
|
|
291
|
+
userId: principal.userId,
|
|
292
|
+
tokenId: principal.tokenId,
|
|
293
|
+
meta: context.client
|
|
294
|
+
};
|
|
295
|
+
if (principal.kind === "oauth") return {
|
|
296
|
+
kind: "oauth_client",
|
|
297
|
+
userId: principal.userId,
|
|
298
|
+
tokenId: null,
|
|
299
|
+
meta: {
|
|
300
|
+
...context.client,
|
|
301
|
+
oauthClient: principal.clientId
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
throw internalError$1(context, `A write was routed for a principal of kind ${principal.kind}; only a person writes.`);
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/ops/assets.ts
|
|
308
|
+
const NO_BUCKET = {
|
|
309
|
+
message: "This Hollware keeps no images: its Worker has no R2 bucket bound as MEDIA.",
|
|
310
|
+
fix: "Add { \"binding\": \"MEDIA\", \"bucket_name\": \"<name>\" } to r2_buckets in wrangler.jsonc, and run hollware deploy, which creates the bucket."
|
|
311
|
+
};
|
|
312
|
+
/** The keys of an asset's files in R2: the file served, and the file as it was uploaded. */
|
|
313
|
+
const servedKey = (site, id) => `sites/${site}/${id}`;
|
|
314
|
+
const originalKey = (site, id) => `originals/${site}/${id}`;
|
|
315
|
+
/** An asset as the operations answer it. */
|
|
316
|
+
function assetView(row, media) {
|
|
317
|
+
return {
|
|
318
|
+
id: row.id,
|
|
319
|
+
reference: `asset:${row.id}`,
|
|
320
|
+
url: `${media.baseUrl}${row.site}/${row.id}`,
|
|
321
|
+
format: row.format,
|
|
322
|
+
width: row.width,
|
|
323
|
+
height: row.height,
|
|
324
|
+
bytes: row.bytes,
|
|
325
|
+
originalFormat: row.originalFormat,
|
|
326
|
+
originalBytes: row.originalBytes,
|
|
327
|
+
alt: row.alt,
|
|
328
|
+
filename: row.filename,
|
|
329
|
+
createdAt: row.createdAt
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function bytesOfBase64(data) {
|
|
333
|
+
const clean = data.replace(/\s/g, "");
|
|
334
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(clean) || clean.length % 4 !== 0) return null;
|
|
335
|
+
const binary = atob(clean);
|
|
336
|
+
const bytes = new Uint8Array(binary.length);
|
|
337
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
338
|
+
return bytes;
|
|
339
|
+
}
|
|
340
|
+
async function sha256Of(bytes) {
|
|
341
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
342
|
+
}
|
|
343
|
+
/** The error code of the Images binding, such as 9412 (not an image) or 9422 (no allowance left). */
|
|
344
|
+
function imagesCode(error) {
|
|
345
|
+
const code = typeof error === "object" && error !== null ? Reflect.get(error, "code") : null;
|
|
346
|
+
if (typeof code === "number") return code;
|
|
347
|
+
const match = /\b(9\d{3})\b/.exec(error instanceof Error ? error.message : String(error));
|
|
348
|
+
return match === null ? null : Number(match[1]);
|
|
349
|
+
}
|
|
350
|
+
const refused$1 = (message, fix) => ({
|
|
351
|
+
outcome: "refused",
|
|
352
|
+
problem: {
|
|
353
|
+
message,
|
|
354
|
+
fix
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
const NOT_READABLE = refused$1("Cloudflare Images could not read this file as an image: it may be damaged or cut short.", "Open it in an image editor, save it again, and upload that.");
|
|
358
|
+
const NO_ALLOWANCE = {
|
|
359
|
+
outcome: "unavailable",
|
|
360
|
+
problem: {
|
|
361
|
+
message: "This month's allowance of image conversions is used up (5,000 on Cloudflare's Free plan): nothing was kept.",
|
|
362
|
+
fix: "Upload it next month, set the media format of the site to \"original\" to keep files as they come, or move the account to the Paid plan of Images."
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const uploadAsset = async (input, context) => {
|
|
366
|
+
const media = context.media ?? null;
|
|
367
|
+
if (media === null) return {
|
|
368
|
+
outcome: "unavailable",
|
|
369
|
+
problem: NO_BUCKET
|
|
370
|
+
};
|
|
371
|
+
const original = bytesOfBase64(input.data);
|
|
372
|
+
if (original === null || original.length === 0) return refused$1("data is not base64.", "Send the file encoded in base64, with its padding.");
|
|
373
|
+
if (original.length > MAX_ASSET_BYTES) return refused$1(`The file is ${original.length} bytes, more than the ${MAX_ASSET_BYTES} an image may be.`, "Make it smaller (fewer pixels, or a format such as WebP) and upload it again.");
|
|
374
|
+
const originalFormat = sniffImage(original);
|
|
375
|
+
if (originalFormat === null) return refused$1("This file is not an image Hollware takes: JPEG, PNG, WebP, AVIF, GIF or SVG, judged by its bytes.", "Export it as one of those formats and upload it again.");
|
|
376
|
+
const sha256 = await sha256Of(original);
|
|
377
|
+
const id = assetIdOf(sha256);
|
|
378
|
+
const site = context.site.key;
|
|
379
|
+
const existing = await media.catalog.byId(site, id);
|
|
380
|
+
if (existing !== null) return {
|
|
381
|
+
outcome: "exists",
|
|
382
|
+
asset: assetView(existing, media)
|
|
383
|
+
};
|
|
384
|
+
let served = original;
|
|
385
|
+
let format = originalFormat;
|
|
386
|
+
let width = null;
|
|
387
|
+
let height = null;
|
|
388
|
+
if (originalFormat === "image/svg+xml") {
|
|
389
|
+
const problem = checkSvg(new TextDecoder().decode(original));
|
|
390
|
+
if (problem !== null) return refused$1(`This SVG is refused: ${problem.reason}.`, problem.fix);
|
|
391
|
+
} else if (media.images !== null) try {
|
|
392
|
+
const info = await media.images.info(original);
|
|
393
|
+
if ("width" in info) {
|
|
394
|
+
width = info.width;
|
|
395
|
+
height = info.height;
|
|
396
|
+
}
|
|
397
|
+
const conversion = conversionOf(originalFormat, width, context.site.media);
|
|
398
|
+
if (conversion !== null) {
|
|
399
|
+
const converted = await media.images.convert(original, conversion);
|
|
400
|
+
served = converted.bytes;
|
|
401
|
+
format = converted.format;
|
|
402
|
+
if (conversion.width !== void 0 && width !== null && height !== null) {
|
|
403
|
+
height = Math.max(1, Math.round(height * conversion.width / width));
|
|
404
|
+
width = conversion.width;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
const code = imagesCode(error);
|
|
409
|
+
if (code === 9422) return NO_ALLOWANCE;
|
|
410
|
+
if (code === 9412) return NOT_READABLE;
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
await media.files.put(servedKey(site, id), served, format);
|
|
414
|
+
await media.files.put(originalKey(site, id), original, originalFormat);
|
|
415
|
+
const { row, created } = await media.catalog.insert({
|
|
416
|
+
id,
|
|
417
|
+
site,
|
|
418
|
+
sha256,
|
|
419
|
+
format,
|
|
420
|
+
width,
|
|
421
|
+
height,
|
|
422
|
+
bytes: served.length,
|
|
423
|
+
originalFormat,
|
|
424
|
+
originalBytes: original.length,
|
|
425
|
+
alt: input.alt,
|
|
426
|
+
filename: input.filename,
|
|
427
|
+
createdBy: actorOf$1(context).userId,
|
|
428
|
+
createdAt: context.clock.now().toISOString()
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
outcome: created ? "created" : "exists",
|
|
432
|
+
asset: assetView(row, media)
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
/** The cursor of a page: the `createdAt` and id of its last row, in base64url. */
|
|
436
|
+
const cursorOf = (row) => btoa(`${row.createdAt}|${row.id}`).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
437
|
+
function parseCursor(cursor) {
|
|
438
|
+
if (cursor === void 0) return null;
|
|
439
|
+
try {
|
|
440
|
+
const padded = cursor.replace(/-/g, "+").replace(/_/g, "/");
|
|
441
|
+
const [createdAt, id] = atob(padded + "=".repeat((4 - padded.length % 4) % 4)).split("|");
|
|
442
|
+
return createdAt !== void 0 && id !== void 0 ? {
|
|
443
|
+
createdAt,
|
|
444
|
+
id
|
|
445
|
+
} : null;
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const listAssets = async (input, context) => {
|
|
451
|
+
const media = context.media ?? null;
|
|
452
|
+
if (media === null) return {
|
|
453
|
+
outcome: "ok",
|
|
454
|
+
assets: [],
|
|
455
|
+
nextCursor: null
|
|
456
|
+
};
|
|
457
|
+
const rows = await media.catalog.list(context.site.key, input.limit + 1, parseCursor(input.cursor));
|
|
458
|
+
const page = rows.slice(0, input.limit);
|
|
459
|
+
const last = page.at(-1);
|
|
460
|
+
return {
|
|
461
|
+
outcome: "ok",
|
|
462
|
+
assets: page.map((row) => assetView(row, media)),
|
|
463
|
+
nextCursor: rows.length > input.limit && last !== void 0 ? cursorOf(last) : null
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
const getAsset = async (input, context) => {
|
|
467
|
+
const media = context.media ?? null;
|
|
468
|
+
if (media === null) return { outcome: "not_found" };
|
|
469
|
+
const row = await media.catalog.byId(context.site.key, input.id);
|
|
470
|
+
return row === null ? { outcome: "not_found" } : {
|
|
471
|
+
outcome: "ok",
|
|
472
|
+
asset: assetView(row, media)
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* The URL an `asset:` reference of the site resolves to: where the Worker serves the image, or
|
|
477
|
+
* the reference as it is when the Worker serves no media.
|
|
478
|
+
*/
|
|
479
|
+
function assetUrlOf(context) {
|
|
480
|
+
const base = context.media?.baseUrl;
|
|
481
|
+
return (id) => base === void 0 ? `asset:${id}` : `${base}${context.site.key}/${id}`;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* A value as a read of the published site sees it (RPC, the read API): every `asset:` reference
|
|
485
|
+
* in it as the image's URL. Over MCP, an agent reads what it wrote.
|
|
486
|
+
*/
|
|
487
|
+
function forReaders(context, value) {
|
|
488
|
+
return context.view === "published" ? resolveAssetReferences(value, assetUrlOf(context)) : value;
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
260
491
|
//#region src/db/d1.ts
|
|
261
492
|
/**
|
|
262
493
|
* Drizzle's D1 driver is typed over a `D1Database`, but at runtime it only calls `prepare` and
|
|
@@ -2074,7 +2305,7 @@ function boundedJson(value) {
|
|
|
2074
2305
|
* The actor of a principal. A person signed in to the admin is a `user`, with the admin as their
|
|
2075
2306
|
* client; any other OAuth client is an `oauth_client`; a personal or read token is a `token`.
|
|
2076
2307
|
*/
|
|
2077
|
-
function actorOf
|
|
2308
|
+
function actorOf(principal, meta, adminClientId) {
|
|
2078
2309
|
if (principal.kind === "oauth") return {
|
|
2079
2310
|
kind: principal.clientId === adminClientId ? "user" : "oauth_client",
|
|
2080
2311
|
userId: principal.userId,
|
|
@@ -2398,7 +2629,7 @@ function createSecretStore(db, now) {
|
|
|
2398
2629
|
* went wrong goes to the log and never to the caller, whose agent could repeat it to a person or
|
|
2399
2630
|
* to another system. The request id is how the operator finds the log line.
|
|
2400
2631
|
*/
|
|
2401
|
-
function internalError
|
|
2632
|
+
function internalError(requestId, cause) {
|
|
2402
2633
|
return new HollwareError("internal", "The operation failed on the server.", `Try again. If it keeps failing, give the site operator this request id: ${requestId}.`, { cause });
|
|
2403
2634
|
}
|
|
2404
2635
|
/** The fix of `timeout`, by effect: a write that ran out of time may still finish. */
|
|
@@ -3532,7 +3763,8 @@ const NAMING_FIELDS = [
|
|
|
3532
3763
|
"id",
|
|
3533
3764
|
"entryId",
|
|
3534
3765
|
"reviewId",
|
|
3535
|
-
"name"
|
|
3766
|
+
"name",
|
|
3767
|
+
"filename"
|
|
3536
3768
|
];
|
|
3537
3769
|
const FIELD_MAX = 200;
|
|
3538
3770
|
function isAudited(name) {
|
|
@@ -3545,7 +3777,7 @@ function idOf(value) {
|
|
|
3545
3777
|
const id = record(value)["id"];
|
|
3546
3778
|
return typeof id === "string" ? id : null;
|
|
3547
3779
|
}
|
|
3548
|
-
/** What the call acted on: the entry, review or
|
|
3780
|
+
/** What the call acted on: the entry, review, comment or asset of its answer, else its site. */
|
|
3549
3781
|
function targetOf(output, site) {
|
|
3550
3782
|
const entry = idOf(output["entry"]);
|
|
3551
3783
|
if (entry !== null) return {
|
|
@@ -3562,6 +3794,11 @@ function targetOf(output, site) {
|
|
|
3562
3794
|
type: "comment",
|
|
3563
3795
|
id: comment
|
|
3564
3796
|
};
|
|
3797
|
+
const asset = idOf(output["asset"]);
|
|
3798
|
+
if (asset !== null) return {
|
|
3799
|
+
type: "asset",
|
|
3800
|
+
id: asset
|
|
3801
|
+
};
|
|
3565
3802
|
return {
|
|
3566
3803
|
type: "site",
|
|
3567
3804
|
id: site
|
|
@@ -3704,6 +3941,116 @@ function semanticOfEnv(env) {
|
|
|
3704
3941
|
return semanticOf(env, typeof model === "string" && model !== "" ? model : DEFAULT_EMBEDDING_MODEL);
|
|
3705
3942
|
}
|
|
3706
3943
|
//#endregion
|
|
3944
|
+
//#region src/media/cloudflare.ts
|
|
3945
|
+
const streamOf = (bytes) => new Blob([bytes]).stream();
|
|
3946
|
+
/** R2 as the store of asset files. */
|
|
3947
|
+
function r2Files(bucket) {
|
|
3948
|
+
return {
|
|
3949
|
+
async put(key, bytes, contentType) {
|
|
3950
|
+
await bucket.put(key, bytes, { httpMetadata: { contentType } });
|
|
3951
|
+
},
|
|
3952
|
+
async get(key) {
|
|
3953
|
+
const object = await bucket.get(key);
|
|
3954
|
+
if (object === null) return null;
|
|
3955
|
+
return {
|
|
3956
|
+
body: object.body,
|
|
3957
|
+
contentType: object.httpMetadata?.contentType ?? "application/octet-stream"
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
function imagesConverter(images) {
|
|
3963
|
+
return {
|
|
3964
|
+
async info(bytes) {
|
|
3965
|
+
const info = await images.info(streamOf(bytes));
|
|
3966
|
+
if (typeof info !== "object" || info === null) throw new Error("Images answered no info");
|
|
3967
|
+
const format = Reflect.get(info, "format");
|
|
3968
|
+
if (format === "image/svg+xml") return { format };
|
|
3969
|
+
const width = Reflect.get(info, "width");
|
|
3970
|
+
const height = Reflect.get(info, "height");
|
|
3971
|
+
if (typeof format !== "string" || typeof width !== "number" || typeof height !== "number") throw new Error("Images answered an info without format, width and height");
|
|
3972
|
+
return {
|
|
3973
|
+
format,
|
|
3974
|
+
width,
|
|
3975
|
+
height
|
|
3976
|
+
};
|
|
3977
|
+
},
|
|
3978
|
+
async convert(bytes, conversion) {
|
|
3979
|
+
let transformer = images.input(streamOf(bytes));
|
|
3980
|
+
if (conversion.width !== void 0) transformer = transformer.transform({
|
|
3981
|
+
width: conversion.width,
|
|
3982
|
+
fit: "scale-down"
|
|
3983
|
+
});
|
|
3984
|
+
const result = await transformer.output({
|
|
3985
|
+
format: conversion.format,
|
|
3986
|
+
quality: conversion.quality,
|
|
3987
|
+
anim: conversion.anim
|
|
3988
|
+
});
|
|
3989
|
+
return {
|
|
3990
|
+
bytes: new Uint8Array(await new Response(result.image()).arrayBuffer()),
|
|
3991
|
+
format: result.contentType()
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
const rowOf = (row) => ({
|
|
3997
|
+
id: row.id,
|
|
3998
|
+
site: row.site_id,
|
|
3999
|
+
sha256: row.sha256,
|
|
4000
|
+
format: row.format,
|
|
4001
|
+
width: row.width,
|
|
4002
|
+
height: row.height,
|
|
4003
|
+
bytes: row.bytes,
|
|
4004
|
+
originalFormat: row.original_format,
|
|
4005
|
+
originalBytes: row.original_bytes,
|
|
4006
|
+
alt: row.alt,
|
|
4007
|
+
filename: row.filename,
|
|
4008
|
+
createdBy: row.created_by,
|
|
4009
|
+
createdAt: row.created_at
|
|
4010
|
+
});
|
|
4011
|
+
/** The rows of `assets` in D1, always of one site. */
|
|
4012
|
+
function d1Catalog(db) {
|
|
4013
|
+
const byId = async (site, id) => {
|
|
4014
|
+
const row = await db.prepare("SELECT * FROM assets WHERE site_id = ? AND id = ?").bind(site, id).first();
|
|
4015
|
+
return row === null ? null : rowOf(row);
|
|
4016
|
+
};
|
|
4017
|
+
return {
|
|
4018
|
+
byId,
|
|
4019
|
+
async insert(asset) {
|
|
4020
|
+
const written = await db.prepare(`INSERT INTO assets (id, site_id, sha256, format, width, height, bytes, original_format,
|
|
4021
|
+
original_bytes, alt, filename, created_by, created_at, updated_at)
|
|
4022
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4023
|
+
ON CONFLICT (site_id, sha256) DO NOTHING
|
|
4024
|
+
RETURNING *`).bind(asset.id, asset.site, asset.sha256, asset.format, asset.width, asset.height, asset.bytes, asset.originalFormat, asset.originalBytes, asset.alt, asset.filename, asset.createdBy, asset.createdAt, asset.createdAt).first();
|
|
4025
|
+
if (written !== null) return {
|
|
4026
|
+
row: rowOf(written),
|
|
4027
|
+
created: true
|
|
4028
|
+
};
|
|
4029
|
+
const existing = await db.prepare("SELECT * FROM assets WHERE site_id = ? AND sha256 = ?").bind(asset.site, asset.sha256).first();
|
|
4030
|
+
if (existing === null) throw new Error(`No asset of ${asset.sha256} after a conflict`);
|
|
4031
|
+
return {
|
|
4032
|
+
row: rowOf(existing),
|
|
4033
|
+
created: false
|
|
4034
|
+
};
|
|
4035
|
+
},
|
|
4036
|
+
async list(site, limit, cursor) {
|
|
4037
|
+
const { results } = await (cursor === null ? db.prepare("SELECT * FROM assets WHERE site_id = ? ORDER BY created_at DESC, id DESC LIMIT ?").bind(site, limit) : db.prepare(`SELECT * FROM assets WHERE site_id = ? AND (created_at, id) < (?, ?)
|
|
4038
|
+
ORDER BY created_at DESC, id DESC LIMIT ?`).bind(site, cursor.createdAt, cursor.id, limit)).all();
|
|
4039
|
+
return results.map(rowOf);
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
/** Media of a Worker: `null` without its R2 bucket, and without conversion when `IMAGES` is not bound. */
|
|
4044
|
+
function mediaOfEnv(env) {
|
|
4045
|
+
if (env.MEDIA === void 0 || env.HOLLWARE_PUBLIC_URL === void 0) return null;
|
|
4046
|
+
return {
|
|
4047
|
+
files: r2Files(env.MEDIA),
|
|
4048
|
+
images: env.IMAGES === void 0 ? null : imagesConverter(env.IMAGES),
|
|
4049
|
+
catalog: d1Catalog(env.DB),
|
|
4050
|
+
baseUrl: `${env.HOLLWARE_PUBLIC_URL.replace(/\/$/, "")}/assets/`
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
//#endregion
|
|
3707
4054
|
//#region src/analytics/cloudflare.ts
|
|
3708
4055
|
/** The dataset of Analytics Engine when the Worker names none. */
|
|
3709
4056
|
const DEFAULT_ANALYTICS_DATASET = "hollware_page_views";
|
|
@@ -3863,6 +4210,7 @@ function installOpsApi(app, options) {
|
|
|
3863
4210
|
notify: options.notify(c, requestId, site.key),
|
|
3864
4211
|
semantic: semanticOfEnv(c.env),
|
|
3865
4212
|
analytics: analyticsReaderOf(c.env),
|
|
4213
|
+
media: mediaOfEnv(c.env),
|
|
3866
4214
|
...options.structure === void 0 ? {} : { structure: options.structure(c.env.DB, c.env.HOLLWARE_SECRET) }
|
|
3867
4215
|
};
|
|
3868
4216
|
/** The line of the call, after it answered or failed. */
|
|
@@ -4188,7 +4536,7 @@ async function writeForPerson(db, log, siteOf, userId, line) {
|
|
|
4188
4536
|
* The ceremonies, with SimpleWebAuthn and its parsers, in a chunk of their own: only these four
|
|
4189
4537
|
* routes use them, and a Worker that serves MCP never pays for them at startup (`budgets.json`).
|
|
4190
4538
|
*/
|
|
4191
|
-
const ceremonies = () => import("./ceremonies-
|
|
4539
|
+
const ceremonies = () => import("./ceremonies-zTn1U6BD.mjs");
|
|
4192
4540
|
/**
|
|
4193
4541
|
* The cookie of a signed-in person: only `/authorize` reads it (ADR-026), and the sign-out below
|
|
4194
4542
|
* it, `/authorize/signout`, which the path of the cookie reaches too.
|
|
@@ -4819,7 +5167,7 @@ function createGateway(options) {
|
|
|
4819
5167
|
* The activity log of a request (ADR-037), for an actor known once the client has said what it
|
|
4820
5168
|
* is. A person in the admin is a `user`; any other OAuth client, an `oauth_client`.
|
|
4821
5169
|
*/
|
|
4822
|
-
const auditOf = (c, principal, adminClientId) => (meta) => auditAs(c, actorOf
|
|
5170
|
+
const auditOf = (c, principal, adminClientId) => (meta) => auditAs(c, actorOf(principal, meta, adminClientId));
|
|
4823
5171
|
/**
|
|
4824
5172
|
* A protected route: the resource server of `resource` answers a request without a token and a
|
|
4825
5173
|
* token it refuses (401, logged with the reason), and `proceed` gets the principal of the rest.
|
|
@@ -4889,9 +5237,10 @@ function createGateway(options) {
|
|
|
4889
5237
|
c.set("requestId", crypto.randomUUID());
|
|
4890
5238
|
await next();
|
|
4891
5239
|
const https = c.env.HOLLWARE_PUBLIC_URL?.startsWith("https://") === true;
|
|
4892
|
-
if (!(!c.res.headers.has("x-content-type-options") || https && !c.res.headers.has("strict-transport-security"))) return;
|
|
5240
|
+
if (!(!c.res.headers.has("x-content-type-options") || !c.res.headers.has("x-robots-tag") || https && !c.res.headers.has("strict-transport-security"))) return;
|
|
4893
5241
|
const headers = new Headers(c.res.headers);
|
|
4894
5242
|
if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff");
|
|
5243
|
+
if (!headers.has("x-robots-tag")) headers.set("x-robots-tag", "noindex, nofollow");
|
|
4895
5244
|
if (https && !headers.has("strict-transport-security")) headers.set("strict-transport-security", "max-age=31536000");
|
|
4896
5245
|
c.res = new Response(c.res.body, {
|
|
4897
5246
|
status: c.res.status,
|
|
@@ -4938,6 +5287,10 @@ function createGateway(options) {
|
|
|
4938
5287
|
await options.refresh?.(c.env.DB);
|
|
4939
5288
|
await next();
|
|
4940
5289
|
});
|
|
5290
|
+
app.get("/robots.txt", () => new Response("User-agent: *\nDisallow: /\n", { headers: {
|
|
5291
|
+
"content-type": "text/plain; charset=utf-8",
|
|
5292
|
+
"cache-control": "public, max-age=86400"
|
|
5293
|
+
} }));
|
|
4941
5294
|
app.get("/health", () => Response.json({ ok: true }, { headers: { "cache-control": "no-store" } }));
|
|
4942
5295
|
app.all("/mcp", async (c) => protectWrites(c, "mcp", async (principal) => {
|
|
4943
5296
|
const requestId = c.get("requestId");
|
|
@@ -4955,7 +5308,8 @@ function createGateway(options) {
|
|
|
4955
5308
|
notify: notify(c, requestId, site.key),
|
|
4956
5309
|
semantic: semanticOfEnv(c.env),
|
|
4957
5310
|
analytics: analyticsReaderOf(c.env),
|
|
4958
|
-
structure: structureOf(db, c.env.HOLLWARE_SECRET)
|
|
5311
|
+
structure: structureOf(db, c.env.HOLLWARE_SECRET),
|
|
5312
|
+
media: mediaOfEnv(c.env)
|
|
4959
5313
|
},
|
|
4960
5314
|
...principal.kind === "read" ? {} : { audit: {
|
|
4961
5315
|
log: auditOf(c, principal, null),
|
|
@@ -5049,6 +5403,45 @@ function createGateway(options) {
|
|
|
5049
5403
|
structure: structureOf,
|
|
5050
5404
|
audit: async (c, principal) => auditOf(c, principal, (await adminClient(oauth(c.env.HOLLWARE_PUBLIC_URL), c.env)).clientId)
|
|
5051
5405
|
});
|
|
5406
|
+
app.get("/assets/:site/:id", async (c) => {
|
|
5407
|
+
const { site, id } = c.req.param();
|
|
5408
|
+
const media = mediaOfEnv(c.env);
|
|
5409
|
+
const file = media === null || !isSiteKey(site) || !ASSET_ID.test(id) ? null : await media.files.get(servedKey(site, id));
|
|
5410
|
+
if (file === null) return Response.json({
|
|
5411
|
+
code: "not_found",
|
|
5412
|
+
message: "There is no image at this URL.",
|
|
5413
|
+
fix: "Use the url that upload_asset or get_asset gave the image."
|
|
5414
|
+
}, {
|
|
5415
|
+
status: 404,
|
|
5416
|
+
headers: { "cache-control": "no-store" }
|
|
5417
|
+
});
|
|
5418
|
+
const headers = new Headers({
|
|
5419
|
+
"content-type": file.contentType,
|
|
5420
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
5421
|
+
"cross-origin-resource-policy": "cross-origin"
|
|
5422
|
+
});
|
|
5423
|
+
if (file.contentType === "image/svg+xml") headers.set("content-security-policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox");
|
|
5424
|
+
return new Response(file.body, { headers });
|
|
5425
|
+
});
|
|
5426
|
+
app.get("/v1/assets/:id/original", (c) => protectWrites(c, "v1", async (principal) => {
|
|
5427
|
+
if (missingScopes(principal, ["content:read"]).length > 0) return insufficientScope(["content:read"], oauth(c.env.HOLLWARE_PUBLIC_URL).metadataUrls.v1);
|
|
5428
|
+
const id = c.req.param("id");
|
|
5429
|
+
const media = mediaOfEnv(c.env);
|
|
5430
|
+
const row = media === null || !ASSET_ID.test(id) ? null : await media.catalog.byId(principal.site, id);
|
|
5431
|
+
const file = media === null || row === null ? null : await media.files.get(originalKey(row.site, row.id));
|
|
5432
|
+
if (file === null) return Response.json({
|
|
5433
|
+
code: "not_found",
|
|
5434
|
+
message: `The site has no image ${id}.`,
|
|
5435
|
+
fix: "List the images of the site with list_assets."
|
|
5436
|
+
}, {
|
|
5437
|
+
status: 404,
|
|
5438
|
+
headers: { "cache-control": "no-store" }
|
|
5439
|
+
});
|
|
5440
|
+
return new Response(file.body, { headers: {
|
|
5441
|
+
"content-type": file.contentType,
|
|
5442
|
+
"cache-control": "private, no-store"
|
|
5443
|
+
} });
|
|
5444
|
+
}));
|
|
5052
5445
|
if (options.read !== void 0) installReadApi(app, {
|
|
5053
5446
|
read: options.read,
|
|
5054
5447
|
corsOrigins: (site) => sites.get(site)?.cors.origins ?? [],
|
|
@@ -5063,6 +5456,8 @@ function createGateway(options) {
|
|
|
5063
5456
|
return await app.fetch(request, env, ctx);
|
|
5064
5457
|
};
|
|
5065
5458
|
}
|
|
5459
|
+
/** The id of an asset: `ast_` and 26 hexadecimal characters, from the hash of its file. */
|
|
5460
|
+
const ASSET_ID = /^ast_[0-9a-f]{26}$/;
|
|
5066
5461
|
/** The group of a route that is rate limited, or `null` for one that is not (T-10). */
|
|
5067
5462
|
function limitedRoute(method, pathname) {
|
|
5068
5463
|
if (method !== "POST") return null;
|
|
@@ -5261,6 +5656,9 @@ const OUTPUT_TOO_LARGE_FIX = {
|
|
|
5261
5656
|
get_translations: "The translations do not fit in one answer. Read each one with get_entry instead.",
|
|
5262
5657
|
list_entries: "Ask for less: pass a smaller limit, or narrow the list with locale, tag or status.",
|
|
5263
5658
|
list_reviews: "Ask for less: pass a smaller limit.",
|
|
5659
|
+
list_assets: "Ask for less: pass a smaller limit.",
|
|
5660
|
+
get_asset: "Nothing to ask for less of: the answer is one asset. Try again.",
|
|
5661
|
+
upload_asset: "The operation ran and the image may have been kept: list the images with list_assets before you upload it again.",
|
|
5264
5662
|
publish: "The operation ran and its change may have been applied: read the entry with get_entry before you repeat it.",
|
|
5265
5663
|
request_changes: "The operation ran and its change may have been applied: read the review with list_reviews before you repeat it.",
|
|
5266
5664
|
request_review: "The operation ran and its review may have been asked: read it with list_reviews before you repeat it.",
|
|
@@ -5535,7 +5933,7 @@ function createMcpEndpoint(options) {
|
|
|
5535
5933
|
try {
|
|
5536
5934
|
result = spec.name === "publish" ? await performPublish() : answerResult(spec, await perform(input));
|
|
5537
5935
|
} catch (error) {
|
|
5538
|
-
failure = error instanceof HollwareError ? error : internalError
|
|
5936
|
+
failure = error instanceof HollwareError ? error : internalError(call.requestId, error);
|
|
5539
5937
|
isError = true;
|
|
5540
5938
|
result = errorResult(failure);
|
|
5541
5939
|
}
|
|
@@ -5727,7 +6125,8 @@ async function run(deps, env, method, site, schema, build, handler, viewOf = ()
|
|
|
5727
6125
|
clock: deps.clock,
|
|
5728
6126
|
client: null,
|
|
5729
6127
|
requestId,
|
|
5730
|
-
semantic: semanticOfEnv(env)
|
|
6128
|
+
semantic: semanticOfEnv(env),
|
|
6129
|
+
media: mediaOfEnv(env)
|
|
5731
6130
|
};
|
|
5732
6131
|
try {
|
|
5733
6132
|
return await handler(input, context);
|
|
@@ -6352,53 +6751,6 @@ const contentStats = async (input, context) => {
|
|
|
6352
6751
|
};
|
|
6353
6752
|
};
|
|
6354
6753
|
//#endregion
|
|
6355
|
-
//#region src/ops/errors.ts
|
|
6356
|
-
/**
|
|
6357
|
-
* The error of something that must never happen: a bug of Hollware, not a mistake of the caller.
|
|
6358
|
-
* Like the `internal` error of the adapters, its message says nothing about the cause, which goes
|
|
6359
|
-
* to the log through `cause`, and its fix names the request id the operator finds the log line by.
|
|
6360
|
-
*/
|
|
6361
|
-
function internalError(context, cause) {
|
|
6362
|
-
return new HollwareError("internal", "The operation failed on the server.", `Try again. If it keeps failing, give the site operator this request id: ${context.requestId}.`, { cause: new Error(cause) });
|
|
6363
|
-
}
|
|
6364
|
-
/**
|
|
6365
|
-
* The error for an answer of the store that the call it came from can never get: an entry that
|
|
6366
|
-
* is created cannot conflict, a publish has no slug to take. It is a bug, not an outcome of the
|
|
6367
|
-
* operation, so it is not one of the outcomes of its spec.
|
|
6368
|
-
*/
|
|
6369
|
-
function unexpectedAnswer(context, call, answer) {
|
|
6370
|
-
return internalError(context, `The store answered ${answer.outcome} to ${call}, which it never does.`);
|
|
6371
|
-
}
|
|
6372
|
-
//#endregion
|
|
6373
|
-
//#region src/ops/actor.ts
|
|
6374
|
-
/**
|
|
6375
|
-
* Who a write is made by: the person behind the token of the call, with what its client said
|
|
6376
|
-
* about itself. A personal token writes as itself (ADR-019); an OAuth access token writes as the
|
|
6377
|
-
* client the person allowed, whose id goes with what the client said (ADR-026). The adapters check
|
|
6378
|
-
* the scopes of the operation before any handler runs and never route a write for the RPC
|
|
6379
|
-
* binding, so reaching one with it is a bug, answered as `internal` before anything is read or
|
|
6380
|
-
* written.
|
|
6381
|
-
*/
|
|
6382
|
-
function actorOf(context) {
|
|
6383
|
-
const { principal } = context;
|
|
6384
|
-
if (principal.kind === "token") return {
|
|
6385
|
-
kind: "token",
|
|
6386
|
-
userId: principal.userId,
|
|
6387
|
-
tokenId: principal.tokenId,
|
|
6388
|
-
meta: context.client
|
|
6389
|
-
};
|
|
6390
|
-
if (principal.kind === "oauth") return {
|
|
6391
|
-
kind: "oauth_client",
|
|
6392
|
-
userId: principal.userId,
|
|
6393
|
-
tokenId: null,
|
|
6394
|
-
meta: {
|
|
6395
|
-
...context.client,
|
|
6396
|
-
oauthClient: principal.clientId
|
|
6397
|
-
}
|
|
6398
|
-
};
|
|
6399
|
-
throw internalError(context, `A write was routed for a principal of kind ${principal.kind}; only a person writes.`);
|
|
6400
|
-
}
|
|
6401
|
-
//#endregion
|
|
6402
6754
|
//#region src/ops/collections.ts
|
|
6403
6755
|
/**
|
|
6404
6756
|
* Which collection and locale a call names, checked against the config of the site before
|
|
@@ -6528,7 +6880,7 @@ function revisionOf(input) {
|
|
|
6528
6880
|
const createDraft = async (input, context) => {
|
|
6529
6881
|
const collection = requireCollection(context.site, input.collection);
|
|
6530
6882
|
requireLocale(context.site, input.locale);
|
|
6531
|
-
const actor = actorOf(context);
|
|
6883
|
+
const actor = actorOf$1(context);
|
|
6532
6884
|
const processed = await processFor(context.site, collection, input.markdown);
|
|
6533
6885
|
if (!processed.ok) return {
|
|
6534
6886
|
outcome: "refused",
|
|
@@ -6605,13 +6957,13 @@ const getEntry$1 = async (input, context) => {
|
|
|
6605
6957
|
return {
|
|
6606
6958
|
outcome: "ok",
|
|
6607
6959
|
entry: {
|
|
6608
|
-
...entrySummary(found),
|
|
6960
|
+
...forReaders(context, entrySummary(found)),
|
|
6609
6961
|
revision: revision.id,
|
|
6610
6962
|
verdict: verdictView(revision.verdict),
|
|
6611
6963
|
lastPublication: publicationView(publication),
|
|
6612
6964
|
...input.formats.includes("markdown") ? { markdown: revision.markdown } : {},
|
|
6613
|
-
...input.formats.includes("document") ? { document: revision.document } : {},
|
|
6614
|
-
...input.formats.includes("html") ? { html: await renderHtml(revision.document) } : {}
|
|
6965
|
+
...input.formats.includes("document") ? { document: forReaders(context, revision.document) } : {},
|
|
6966
|
+
...input.formats.includes("html") ? { html: await renderHtml(resolveAssetReferences(revision.document, assetUrlOf(context))) } : {}
|
|
6615
6967
|
},
|
|
6616
6968
|
...related === void 0 ? {} : { related }
|
|
6617
6969
|
};
|
|
@@ -6635,7 +6987,7 @@ const getTranslations$1 = async (input, context) => {
|
|
|
6635
6987
|
if (group.length === 0) return { outcome: "not_found" };
|
|
6636
6988
|
return {
|
|
6637
6989
|
outcome: "ok",
|
|
6638
|
-
entries: group.map(entrySummary)
|
|
6990
|
+
entries: group.map((entry) => forReaders(context, entrySummary(entry)))
|
|
6639
6991
|
};
|
|
6640
6992
|
};
|
|
6641
6993
|
//#endregion
|
|
@@ -6656,7 +7008,7 @@ const listEntries$1 = async (input, context) => {
|
|
|
6656
7008
|
});
|
|
6657
7009
|
return {
|
|
6658
7010
|
outcome: "ok",
|
|
6659
|
-
entries: page.entries.map(entrySummary),
|
|
7011
|
+
entries: page.entries.map((entry) => forReaders(context, entrySummary(entry))),
|
|
6660
7012
|
nextCursor: page.nextCursor
|
|
6661
7013
|
};
|
|
6662
7014
|
};
|
|
@@ -6741,7 +7093,7 @@ const search$1 = async (input, context) => {
|
|
|
6741
7093
|
outcome: "ok",
|
|
6742
7094
|
mode,
|
|
6743
7095
|
results: hits.map((hit) => ({
|
|
6744
|
-
entry: entrySummary(hit.entry),
|
|
7096
|
+
entry: forReaders(context, entrySummary(hit.entry)),
|
|
6745
7097
|
snippet: hit.snippet,
|
|
6746
7098
|
score: hit.score
|
|
6747
7099
|
}))
|
|
@@ -6794,11 +7146,11 @@ function siteEventFor(input) {
|
|
|
6794
7146
|
/** How many slugs of refused entries an answer names. */
|
|
6795
7147
|
const EXAMPLES = 3;
|
|
6796
7148
|
function structureOf(context) {
|
|
6797
|
-
if (context.structure === void 0) throw internalError(context, "An operation on the structure ran where the structure is not reachable.");
|
|
7149
|
+
if (context.structure === void 0) throw internalError$1(context, "An operation on the structure ran where the structure is not reachable.");
|
|
6798
7150
|
return context.structure;
|
|
6799
7151
|
}
|
|
6800
7152
|
function actor(context) {
|
|
6801
|
-
const { userId, meta } = actorOf(context);
|
|
7153
|
+
const { userId, meta } = actorOf$1(context);
|
|
6802
7154
|
return {
|
|
6803
7155
|
userId,
|
|
6804
7156
|
meta
|
|
@@ -6849,7 +7201,7 @@ async function impactOf(context, store, current, next) {
|
|
|
6849
7201
|
/** The definition of the site of the call, as stored, with its version. */
|
|
6850
7202
|
async function currentOf(context) {
|
|
6851
7203
|
const stored = await structureOf(context).store.read(context.site.key);
|
|
6852
|
-
if (stored === null) throw internalError(context, `The site ${context.site.key} is served and has no definition in D1.`);
|
|
7204
|
+
if (stored === null) throw internalError$1(context, `The site ${context.site.key} is served and has no definition in D1.`);
|
|
6853
7205
|
return {
|
|
6854
7206
|
version: stored.version,
|
|
6855
7207
|
definition: siteDefinitionData.parse(stored.definition)
|
|
@@ -7027,7 +7379,7 @@ const publish$1 = async (input, context) => {
|
|
|
7027
7379
|
const mayPublish = context.principal.scopes.includes("content:publish");
|
|
7028
7380
|
if (!mayPublish && (input.group.length > 0 || input.publishAt !== void 0)) throw new HollwareError("invalid_input", "Without content:publish, publish asks for a review of one entry, now: not of a group, and not at a time.", "Publish each entry on its own, without group and publishAt; a person who may publish approves it.");
|
|
7029
7381
|
if (input.group.length > 0) return publishGroup(input, context);
|
|
7030
|
-
const actor = actorOf(context);
|
|
7382
|
+
const actor = actorOf$1(context);
|
|
7031
7383
|
const found = await context.store.findEntryById(context.site.key, input.id, "head");
|
|
7032
7384
|
if (found === null) return { outcome: "not_found" };
|
|
7033
7385
|
const { entry, revision } = found;
|
|
@@ -7173,7 +7525,7 @@ function asPublished(lookup, ids) {
|
|
|
7173
7525
|
*/
|
|
7174
7526
|
async function publishGroup(input, context) {
|
|
7175
7527
|
if (input.publishAt !== void 0) throw new HollwareError("invalid_input", "A group cannot be scheduled: publishAt schedules one entry.", "Publish the group now, without publishAt, or schedule its entries one by one.");
|
|
7176
|
-
const actor = actorOf(context);
|
|
7528
|
+
const actor = actorOf$1(context);
|
|
7177
7529
|
const members = [{
|
|
7178
7530
|
id: input.id,
|
|
7179
7531
|
version: input.version
|
|
@@ -7360,7 +7712,7 @@ const approveReview = async (input, context) => {
|
|
|
7360
7712
|
};
|
|
7361
7713
|
/** Closes a pending review with a note, without publishing. */
|
|
7362
7714
|
const requestChanges = async (input, context) => {
|
|
7363
|
-
const { userId } = actorOf(context);
|
|
7715
|
+
const { userId } = actorOf$1(context);
|
|
7364
7716
|
const closed = await context.store.requestChanges(context.site.key, {
|
|
7365
7717
|
id: input.id,
|
|
7366
7718
|
note: input.note,
|
|
@@ -7379,7 +7731,7 @@ const requestChanges = async (input, context) => {
|
|
|
7379
7731
|
const commentReview = async (input, context) => {
|
|
7380
7732
|
const answer = await context.store.commentReview(context.site.key, {
|
|
7381
7733
|
reviewId: input.id,
|
|
7382
|
-
actor: actorOf(context),
|
|
7734
|
+
actor: actorOf$1(context),
|
|
7383
7735
|
body: input.body
|
|
7384
7736
|
});
|
|
7385
7737
|
return answer.outcome === "commented" ? {
|
|
@@ -7475,7 +7827,7 @@ const restoreEntry = async (input, context) => {
|
|
|
7475
7827
|
* leaves an entry that is not published as it is.
|
|
7476
7828
|
*/
|
|
7477
7829
|
const unpublish = async (input, context) => {
|
|
7478
|
-
const actor = actorOf(context);
|
|
7830
|
+
const actor = actorOf$1(context);
|
|
7479
7831
|
const found = await context.store.findEntryById(context.site.key, input.id, "head");
|
|
7480
7832
|
if (found === null) return { outcome: "not_found" };
|
|
7481
7833
|
const { entry } = found;
|
|
@@ -7519,7 +7871,7 @@ const unpublish = async (input, context) => {
|
|
|
7519
7871
|
* no revision and no version, which is what makes importing the same files twice a no-op.
|
|
7520
7872
|
*/
|
|
7521
7873
|
const updateDraft = async (input, context) => {
|
|
7522
|
-
const actor = actorOf(context);
|
|
7874
|
+
const actor = actorOf$1(context);
|
|
7523
7875
|
const found = await context.store.findEntryById(context.site.key, input.id, "head");
|
|
7524
7876
|
if (found === null) return { outcome: "not_found" };
|
|
7525
7877
|
const { entry, revision } = found;
|
|
@@ -7620,9 +7972,11 @@ const handlers = defineHandlers({
|
|
|
7620
7972
|
delete_entry: deleteEntry,
|
|
7621
7973
|
delete_site: deleteSite,
|
|
7622
7974
|
describe_site: describeSite$1,
|
|
7975
|
+
get_asset: getAsset,
|
|
7623
7976
|
get_entry: getEntry$1,
|
|
7624
7977
|
get_structure: getStructure,
|
|
7625
7978
|
get_translations: getTranslations$1,
|
|
7979
|
+
list_assets: listAssets,
|
|
7626
7980
|
list_entries: listEntries$1,
|
|
7627
7981
|
search: search$1,
|
|
7628
7982
|
list_reviews: listReviews,
|
|
@@ -7636,6 +7990,7 @@ const handlers = defineHandlers({
|
|
|
7636
7990
|
unpublish,
|
|
7637
7991
|
update_site: updateSite,
|
|
7638
7992
|
update_draft: updateDraft,
|
|
7993
|
+
upload_asset: uploadAsset,
|
|
7639
7994
|
validate
|
|
7640
7995
|
});
|
|
7641
7996
|
//#endregion
|