@pramen/cms 0.0.50 → 0.0.52
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 +55 -0
- package/dist/index.d.ts +71 -4
- package/dist/index.js +27 -10
- package/package.json +2 -2
- package/src/index.ts +43 -11
package/README.md
CHANGED
|
@@ -467,6 +467,61 @@ index is what turns a concurrent duplicate into a visible failure instead of a s
|
|
|
467
467
|
ambiguous history. For the same reason the delete-and-purge pair is atomic on the DO but not
|
|
468
468
|
on D1.
|
|
469
469
|
|
|
470
|
+
### Locales
|
|
471
|
+
|
|
472
|
+
A deployment declares the locales it publishes in, and the editor renders its i18n surface
|
|
473
|
+
off that — the Translations panel, the Locale field, the per-row locale column:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
const handlers = { ...createCmsHandlers({ locales: ["cs", "en"] }), ... };
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
One locale (or none declared) means monolingual: no i18n chrome anywhere, and the editor
|
|
480
|
+
stops sending `locale` on a page save at all, so nothing can blind-overwrite a field it no
|
|
481
|
+
longer shows. The FIRST entry is the locale a page is stamped with when created without
|
|
482
|
+
one — derived, not a second `defaultLocale` option, because two settings that can disagree
|
|
483
|
+
about the same fact is how a Czech-only site ends up publishing `en`.
|
|
484
|
+
|
|
485
|
+
Declared, not inferred from the data: the only way to create a second locale is
|
|
486
|
+
`createTranslation`, which the editor exposes from inside the Translations panel — so a rule
|
|
487
|
+
like "show i18n once a second locale exists" would mean a monolingual site could never
|
|
488
|
+
become multilingual. `listCmsCapabilities` is what the editor reads; `listLocales` remains a
|
|
489
|
+
DATA query (which locales are actually authored), and the two differ while a locale is
|
|
490
|
+
declared but not yet used.
|
|
491
|
+
|
|
492
|
+
### Which store: D1 or the Durable Object
|
|
493
|
+
|
|
494
|
+
For a CMS behind a site, **D1 is usually the right store** — and it is the only one that
|
|
495
|
+
lets the CMS live inside an Astro site's own Worker, because D1 is a binding while a
|
|
496
|
+
Durable Object has to be exported from the worker entry.
|
|
497
|
+
|
|
498
|
+
What you give up, and it is worth knowing before you pick:
|
|
499
|
+
|
|
500
|
+
- **No atomic multi-statement mutations.** D1 has no interactive transactions, so
|
|
501
|
+
`transaction(fn)` runs `fn` as-is: a mutation that throws midway keeps what it already
|
|
502
|
+
wrote. Single-statement writes are still atomic. The Worker logs a specific error naming
|
|
503
|
+
the handler and how many statements had committed, so a partial write is diagnosable
|
|
504
|
+
rather than looking like an ordinary 500. Where the CMS does several writes in one
|
|
505
|
+
mutation — a revision plus the row it belongs to, a delete plus its revision purge — the
|
|
506
|
+
order is chosen so a failure leaves a benign state, but it is not a rollback.
|
|
507
|
+
- **No live queries.** Those need the Durable Object's socket host. The editor does not use
|
|
508
|
+
them, so this only matters if your own frontend subscribes.
|
|
509
|
+
- **Scheduled publish needs a Cron trigger.** There is no DO alarm to self-drain the outbox,
|
|
510
|
+
so a delayed task runs only when a Cron trigger calls `createPramen().scheduled`:
|
|
511
|
+
|
|
512
|
+
```jsonc
|
|
513
|
+
"triggers": { "crons": ["* * * * *"] }
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Forget it and a scheduled publish simply never fires. The Worker now notices — when a
|
|
517
|
+
request-tail drain leaves a task queued for the future and no Cron drain has ever run, it
|
|
518
|
+
logs once naming the fix, and stops as soon as a Cron drain is seen.
|
|
519
|
+
- **One shared database, no tenant column.** A non-`main` tenant on D1 is refused unless you
|
|
520
|
+
set `PRAMEN_D1_ALLOW_MULTITENANT=true`, because the rows would commingle.
|
|
521
|
+
|
|
522
|
+
Pick the Durable Object store when a mutation must be atomic, when you want live queries, or
|
|
523
|
+
when tenants must be isolated by construction.
|
|
524
|
+
|
|
470
525
|
## Limitations
|
|
471
526
|
|
|
472
527
|
- **Block `fields` are opaque JSON**, so pramen's row/cell-level ACL and relational queries
|
package/dist/index.d.ts
CHANGED
|
@@ -791,8 +791,19 @@ export interface CmsHandlerOpts {
|
|
|
791
791
|
editorRoles?: readonly string[];
|
|
792
792
|
/** Max accepted media upload size in bytes (enforced at the Worker). Default 25 MB. */
|
|
793
793
|
mediaMaxSize?: number;
|
|
794
|
-
/**
|
|
795
|
-
|
|
794
|
+
/** The locales this deployment publishes in, most-preferred first. Default `["en"]`.
|
|
795
|
+
*
|
|
796
|
+
* DECLARED, not inferred. The editor renders its i18n surface — the Translations panel,
|
|
797
|
+
* the Locale field, the per-row locale column — only when there is more than one, and
|
|
798
|
+
* `listCmsCapabilities` is how it finds out. Inferring "is this site multilingual?" from
|
|
799
|
+
* the locales PRESENT IN DATA cannot work: the only way to create a second locale is
|
|
800
|
+
* `createTranslation`, which the editor exposes from inside the very panel that would
|
|
801
|
+
* stay hidden, so a monolingual site could never become multilingual.
|
|
802
|
+
*
|
|
803
|
+
* The first entry is the default stamped on a page created without one, which is why
|
|
804
|
+
* `defaultLocale` is derived from this rather than configured beside it — two options
|
|
805
|
+
* that can disagree about the same fact is how a Czech-only site ends up stamping "en". */
|
|
806
|
+
locales?: readonly string[];
|
|
796
807
|
/** Roles permitted to approve/reject a page in review and publish (the editorial gate).
|
|
797
808
|
* Default `["reviewer", "admin"]`. */
|
|
798
809
|
reviewerRoles?: readonly string[];
|
|
@@ -897,6 +908,20 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
897
908
|
ok: true;
|
|
898
909
|
}>;
|
|
899
910
|
listContentTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
911
|
+
/** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
|
|
912
|
+
* viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
|
|
913
|
+
* runs in `astro:config:setup`, where there is no editor session and shipping a token to
|
|
914
|
+
* CI just to list type names would be the wrong trade.
|
|
915
|
+
*
|
|
916
|
+
* Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
|
|
917
|
+
* `cms_content_types` ("slugs/names are structural, not sensitive"), and
|
|
918
|
+
* `listPublishedPages` already returns the content-type slug of every published page.
|
|
919
|
+
* The projection is deliberately narrow — slug and name only, never the regions or
|
|
920
|
+
* field schema, which describe the editing surface rather than the published site. */
|
|
921
|
+
listPublicContentTypes: import("@pramen/server").Handler<unknown, {
|
|
922
|
+
slug: string;
|
|
923
|
+
name: string;
|
|
924
|
+
}[]>;
|
|
900
925
|
getContentType: import("@pramen/server").Handler<{
|
|
901
926
|
id: string;
|
|
902
927
|
}, Record<string, unknown>>;
|
|
@@ -960,7 +985,21 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
960
985
|
title: string;
|
|
961
986
|
status: string;
|
|
962
987
|
}[]>;
|
|
963
|
-
/**
|
|
988
|
+
/** What this deployment supports, for an editor to render against — the pages-side
|
|
989
|
+
* counterpart to `listCollections`' `supports: [...]`.
|
|
990
|
+
*
|
|
991
|
+
* The editor asks the SERVER what exists rather than being told by its own /config.js:
|
|
992
|
+
* a client flag can hide a control but cannot make the data right, and the two drift
|
|
993
|
+
* the moment someone adds a locale. `multilingual` is the derived answer to the only
|
|
994
|
+
* question the UI actually asks, so each surface doesn't re-derive it from the list. */
|
|
995
|
+
listCmsCapabilities: import("@pramen/server").Handler<unknown, {
|
|
996
|
+
locales: string[];
|
|
997
|
+
defaultLocale: string;
|
|
998
|
+
multilingual: boolean;
|
|
999
|
+
}>;
|
|
1000
|
+
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
1001
|
+
* store — not configuration. `listCmsCapabilities().locales` is what the deployment
|
|
1002
|
+
* declares; these two differ while a locale is declared but not yet authored. */
|
|
964
1003
|
listLocales: import("@pramen/server").Handler<unknown, string[]>;
|
|
965
1004
|
/** Create a block instance and place it into a page region in one call (the common
|
|
966
1005
|
* editor action). Validates the fields against the block type's schema and the region
|
|
@@ -1237,6 +1276,20 @@ export declare const cmsHandlers: {
|
|
|
1237
1276
|
ok: true;
|
|
1238
1277
|
}>;
|
|
1239
1278
|
listContentTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
1279
|
+
/** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
|
|
1280
|
+
* viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
|
|
1281
|
+
* runs in `astro:config:setup`, where there is no editor session and shipping a token to
|
|
1282
|
+
* CI just to list type names would be the wrong trade.
|
|
1283
|
+
*
|
|
1284
|
+
* Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
|
|
1285
|
+
* `cms_content_types` ("slugs/names are structural, not sensitive"), and
|
|
1286
|
+
* `listPublishedPages` already returns the content-type slug of every published page.
|
|
1287
|
+
* The projection is deliberately narrow — slug and name only, never the regions or
|
|
1288
|
+
* field schema, which describe the editing surface rather than the published site. */
|
|
1289
|
+
listPublicContentTypes: import("@pramen/server").Handler<unknown, {
|
|
1290
|
+
slug: string;
|
|
1291
|
+
name: string;
|
|
1292
|
+
}[]>;
|
|
1240
1293
|
getContentType: import("@pramen/server").Handler<{
|
|
1241
1294
|
id: string;
|
|
1242
1295
|
}, Record<string, unknown>>;
|
|
@@ -1300,7 +1353,21 @@ export declare const cmsHandlers: {
|
|
|
1300
1353
|
title: string;
|
|
1301
1354
|
status: string;
|
|
1302
1355
|
}[]>;
|
|
1303
|
-
/**
|
|
1356
|
+
/** What this deployment supports, for an editor to render against — the pages-side
|
|
1357
|
+
* counterpart to `listCollections`' `supports: [...]`.
|
|
1358
|
+
*
|
|
1359
|
+
* The editor asks the SERVER what exists rather than being told by its own /config.js:
|
|
1360
|
+
* a client flag can hide a control but cannot make the data right, and the two drift
|
|
1361
|
+
* the moment someone adds a locale. `multilingual` is the derived answer to the only
|
|
1362
|
+
* question the UI actually asks, so each surface doesn't re-derive it from the list. */
|
|
1363
|
+
listCmsCapabilities: import("@pramen/server").Handler<unknown, {
|
|
1364
|
+
locales: string[];
|
|
1365
|
+
defaultLocale: string;
|
|
1366
|
+
multilingual: boolean;
|
|
1367
|
+
}>;
|
|
1368
|
+
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
1369
|
+
* store — not configuration. `listCmsCapabilities().locales` is what the deployment
|
|
1370
|
+
* declares; these two differ while a locale is declared but not yet authored. */
|
|
1304
1371
|
listLocales: import("@pramen/server").Handler<unknown, string[]>;
|
|
1305
1372
|
/** Create a block instance and place it into a page region in one call (the common
|
|
1306
1373
|
* editor action). Validates the fields against the block type's schema and the region
|
package/dist/index.js
CHANGED
|
@@ -959,7 +959,8 @@ export function createCmsHandlers(opts = {}) {
|
|
|
959
959
|
const editorRoles = opts.editorRoles ?? ["editor", "admin"];
|
|
960
960
|
const editor = { auth: editorRoles };
|
|
961
961
|
const mediaMaxSize = opts.mediaMaxSize ?? 25_000_000;
|
|
962
|
-
const
|
|
962
|
+
const locales = opts.locales && opts.locales.length > 0 ? [...opts.locales] : ["en"];
|
|
963
|
+
const defaultLocale = locales[0];
|
|
963
964
|
const reviewerRoles = opts.reviewerRoles ?? ["reviewer", "admin"];
|
|
964
965
|
const reviewer = { auth: reviewerRoles };
|
|
965
966
|
const previewTtl = opts.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS;
|
|
@@ -1272,6 +1273,20 @@ export function createCmsHandlers(opts = {}) {
|
|
|
1272
1273
|
return { ok: true };
|
|
1273
1274
|
}, { ...reviewer, ...mediaIdInput }),
|
|
1274
1275
|
listContentTypes: query((ctx) => cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } }), viewer),
|
|
1276
|
+
/** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
|
|
1277
|
+
* viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
|
|
1278
|
+
* runs in `astro:config:setup`, where there is no editor session and shipping a token to
|
|
1279
|
+
* CI just to list type names would be the wrong trade.
|
|
1280
|
+
*
|
|
1281
|
+
* Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
|
|
1282
|
+
* `cms_content_types` ("slugs/names are structural, not sensitive"), and
|
|
1283
|
+
* `listPublishedPages` already returns the content-type slug of every published page.
|
|
1284
|
+
* The projection is deliberately narrow — slug and name only, never the regions or
|
|
1285
|
+
* field schema, which describe the editing surface rather than the published site. */
|
|
1286
|
+
listPublicContentTypes: query(async (ctx) => {
|
|
1287
|
+
const rows = await cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } });
|
|
1288
|
+
return rows.map((r) => ({ slug: String(r.slug), name: String(r.name ?? r.slug) }));
|
|
1289
|
+
}),
|
|
1275
1290
|
getContentType: query(async (ctx, input) => {
|
|
1276
1291
|
const rows = await cdb(ctx).find({ from: "cms_content_types", where: { id: input.id }, limit: 1 });
|
|
1277
1292
|
return rows[0] ?? null;
|
|
@@ -1499,7 +1514,17 @@ export function createCmsHandlers(opts = {}) {
|
|
|
1499
1514
|
return o;
|
|
1500
1515
|
},
|
|
1501
1516
|
}),
|
|
1502
|
-
/**
|
|
1517
|
+
/** What this deployment supports, for an editor to render against — the pages-side
|
|
1518
|
+
* counterpart to `listCollections`' `supports: [...]`.
|
|
1519
|
+
*
|
|
1520
|
+
* The editor asks the SERVER what exists rather than being told by its own /config.js:
|
|
1521
|
+
* a client flag can hide a control but cannot make the data right, and the two drift
|
|
1522
|
+
* the moment someone adds a locale. `multilingual` is the derived answer to the only
|
|
1523
|
+
* question the UI actually asks, so each surface doesn't re-derive it from the list. */
|
|
1524
|
+
listCmsCapabilities: query(() => ({ locales, defaultLocale, multilingual: locales.length > 1 }), viewer),
|
|
1525
|
+
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
1526
|
+
* store — not configuration. `listCmsCapabilities().locales` is what the deployment
|
|
1527
|
+
* declares; these two differ while a locale is declared but not yet authored. */
|
|
1503
1528
|
listLocales: query(async (ctx) => {
|
|
1504
1529
|
// Raw exec bypasses the ACL, so the trash filter has to be written out by hand —
|
|
1505
1530
|
// otherwise the editor's locale switcher offers a locale with zero live pages.
|
|
@@ -1872,8 +1897,6 @@ export function createCmsHandlers(opts = {}) {
|
|
|
1872
1897
|
// has no notion of `x-pramen-store`. Minting on the D1 store therefore produces a
|
|
1873
1898
|
// link that 404s forever while the editor reports success — refuse instead of
|
|
1874
1899
|
// handing out a token that cannot work.
|
|
1875
|
-
if (ctx.store === "d1")
|
|
1876
|
-
throw new PramenError("page preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
|
|
1877
1900
|
const db = cdb(ctx);
|
|
1878
1901
|
// Read the page through the ACL first: minting a link is granting access to it, so a
|
|
1879
1902
|
// caller who cannot read the page must not be able to mint a link that can.
|
|
@@ -2948,12 +2971,6 @@ export function createCollectionHandlers(collections, opts = {}) {
|
|
|
2948
2971
|
const secret = previewSecret(ctx.env);
|
|
2949
2972
|
if (!secret)
|
|
2950
2973
|
throw previewUnconfigured(); // fail closed — never mint a forgeable link
|
|
2951
|
-
// Redemption always reaches a Durable Object (callPrivileged -> PRAMEN.get) and has no
|
|
2952
|
-
// notion of `x-pramen-store`, so a link minted on D1 would 404 forever while the
|
|
2953
|
-
// editor reported success.
|
|
2954
|
-
if (ctx.store === "d1") {
|
|
2955
|
-
throw new PramenError("collection preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
|
|
2956
|
-
}
|
|
2957
2974
|
const row = await loadRow(cdb(ctx), c, input.id);
|
|
2958
2975
|
const ttl = Math.max(60, Math.min(input.expiresIn ?? previewTtl, 30 * 24 * 3600));
|
|
2959
2976
|
const exp = Math.floor(Date.now() / 1000) + ttl;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"description": "Optional block/page builder for pramen — Drupal-Paragraphs-style typed blocks in named regions, reusable blocks, scheduled publishing, built entirely from pramen primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@pramen/server": "0.0.
|
|
44
|
+
"@pramen/server": "0.0.52"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": ">=18"
|
package/src/index.ts
CHANGED
|
@@ -1379,8 +1379,19 @@ export interface CmsHandlerOpts {
|
|
|
1379
1379
|
editorRoles?: readonly string[];
|
|
1380
1380
|
/** Max accepted media upload size in bytes (enforced at the Worker). Default 25 MB. */
|
|
1381
1381
|
mediaMaxSize?: number;
|
|
1382
|
-
/**
|
|
1383
|
-
|
|
1382
|
+
/** The locales this deployment publishes in, most-preferred first. Default `["en"]`.
|
|
1383
|
+
*
|
|
1384
|
+
* DECLARED, not inferred. The editor renders its i18n surface — the Translations panel,
|
|
1385
|
+
* the Locale field, the per-row locale column — only when there is more than one, and
|
|
1386
|
+
* `listCmsCapabilities` is how it finds out. Inferring "is this site multilingual?" from
|
|
1387
|
+
* the locales PRESENT IN DATA cannot work: the only way to create a second locale is
|
|
1388
|
+
* `createTranslation`, which the editor exposes from inside the very panel that would
|
|
1389
|
+
* stay hidden, so a monolingual site could never become multilingual.
|
|
1390
|
+
*
|
|
1391
|
+
* The first entry is the default stamped on a page created without one, which is why
|
|
1392
|
+
* `defaultLocale` is derived from this rather than configured beside it — two options
|
|
1393
|
+
* that can disagree about the same fact is how a Czech-only site ends up stamping "en". */
|
|
1394
|
+
locales?: readonly string[];
|
|
1384
1395
|
/** Roles permitted to approve/reject a page in review and publish (the editorial gate).
|
|
1385
1396
|
* Default `["reviewer", "admin"]`. */
|
|
1386
1397
|
reviewerRoles?: readonly string[];
|
|
@@ -1398,7 +1409,8 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
|
|
|
1398
1409
|
const editorRoles = opts.editorRoles ?? ["editor", "admin"];
|
|
1399
1410
|
const editor = { auth: editorRoles };
|
|
1400
1411
|
const mediaMaxSize = opts.mediaMaxSize ?? 25_000_000;
|
|
1401
|
-
const
|
|
1412
|
+
const locales = opts.locales && opts.locales.length > 0 ? [...opts.locales] : ["en"];
|
|
1413
|
+
const defaultLocale = locales[0]!;
|
|
1402
1414
|
const reviewerRoles = opts.reviewerRoles ?? ["reviewer", "admin"];
|
|
1403
1415
|
const reviewer = { auth: reviewerRoles };
|
|
1404
1416
|
const previewTtl = opts.previewTtlSeconds ?? DEFAULT_PREVIEW_TTL_SECONDS;
|
|
@@ -1712,6 +1724,21 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
|
|
|
1712
1724
|
|
|
1713
1725
|
listContentTypes: query((ctx) => cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } }), viewer),
|
|
1714
1726
|
|
|
1727
|
+
/** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
|
|
1728
|
+
* viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
|
|
1729
|
+
* runs in `astro:config:setup`, where there is no editor session and shipping a token to
|
|
1730
|
+
* CI just to list type names would be the wrong trade.
|
|
1731
|
+
*
|
|
1732
|
+
* Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
|
|
1733
|
+
* `cms_content_types` ("slugs/names are structural, not sensitive"), and
|
|
1734
|
+
* `listPublishedPages` already returns the content-type slug of every published page.
|
|
1735
|
+
* The projection is deliberately narrow — slug and name only, never the regions or
|
|
1736
|
+
* field schema, which describe the editing surface rather than the published site. */
|
|
1737
|
+
listPublicContentTypes: query(async (ctx) => {
|
|
1738
|
+
const rows = await cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } });
|
|
1739
|
+
return rows.map((r) => ({ slug: String(r.slug), name: String(r.name ?? r.slug) }));
|
|
1740
|
+
}),
|
|
1741
|
+
|
|
1715
1742
|
getContentType: query(async (ctx, input: { id: string }) => {
|
|
1716
1743
|
const rows = await cdb(ctx).find({ from: "cms_content_types", where: { id: input.id }, limit: 1 });
|
|
1717
1744
|
return rows[0] ?? null;
|
|
@@ -1932,7 +1959,18 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
|
|
|
1932
1959
|
},
|
|
1933
1960
|
}),
|
|
1934
1961
|
|
|
1935
|
-
/**
|
|
1962
|
+
/** What this deployment supports, for an editor to render against — the pages-side
|
|
1963
|
+
* counterpart to `listCollections`' `supports: [...]`.
|
|
1964
|
+
*
|
|
1965
|
+
* The editor asks the SERVER what exists rather than being told by its own /config.js:
|
|
1966
|
+
* a client flag can hide a control but cannot make the data right, and the two drift
|
|
1967
|
+
* the moment someone adds a locale. `multilingual` is the derived answer to the only
|
|
1968
|
+
* question the UI actually asks, so each surface doesn't re-derive it from the list. */
|
|
1969
|
+
listCmsCapabilities: query(() => ({ locales, defaultLocale, multilingual: locales.length > 1 }), viewer),
|
|
1970
|
+
|
|
1971
|
+
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
1972
|
+
* store — not configuration. `listCmsCapabilities().locales` is what the deployment
|
|
1973
|
+
* declares; these two differ while a locale is declared but not yet authored. */
|
|
1936
1974
|
listLocales: query(async (ctx) => {
|
|
1937
1975
|
// Raw exec bypasses the ACL, so the trash filter has to be written out by hand —
|
|
1938
1976
|
// otherwise the editor's locale switcher offers a locale with zero live pages.
|
|
@@ -2291,7 +2329,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
|
|
|
2291
2329
|
// has no notion of `x-pramen-store`. Minting on the D1 store therefore produces a
|
|
2292
2330
|
// link that 404s forever while the editor reports success — refuse instead of
|
|
2293
2331
|
// handing out a token that cannot work.
|
|
2294
|
-
|
|
2332
|
+
|
|
2295
2333
|
const db = cdb(ctx);
|
|
2296
2334
|
// Read the page through the ACL first: minting a link is granting access to it, so a
|
|
2297
2335
|
// caller who cannot read the page must not be able to mint a link that can.
|
|
@@ -3538,12 +3576,6 @@ export function createCollectionHandlers(collections: readonly CollectionDef[],
|
|
|
3538
3576
|
needs(c, "preview");
|
|
3539
3577
|
const secret = previewSecret(ctx.env);
|
|
3540
3578
|
if (!secret) throw previewUnconfigured(); // fail closed — never mint a forgeable link
|
|
3541
|
-
// Redemption always reaches a Durable Object (callPrivileged -> PRAMEN.get) and has no
|
|
3542
|
-
// notion of `x-pramen-store`, so a link minted on D1 would 404 forever while the
|
|
3543
|
-
// editor reported success.
|
|
3544
|
-
if (ctx.store === "d1") {
|
|
3545
|
-
throw new PramenError("collection preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
|
|
3546
|
-
}
|
|
3547
3579
|
const row = await loadRow(cdb(ctx), c, input.id);
|
|
3548
3580
|
const ttl = Math.max(60, Math.min(input.expiresIn ?? previewTtl, 30 * 24 * 3600));
|
|
3549
3581
|
const exp = Math.floor(Date.now() / 1000) + ttl;
|