@kernhq/module-quire 0.12.0 → 0.13.1

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.
Files changed (34) hide show
  1. package/dist/contract/permissions.d.ts.map +1 -1
  2. package/dist/contract/permissions.js +1 -0
  3. package/dist/contract/permissions.js.map +1 -1
  4. package/dist/contract/router.d.ts +92 -6
  5. package/dist/contract/router.d.ts.map +1 -1
  6. package/dist/contract/router.js +94 -6
  7. package/dist/contract/router.js.map +1 -1
  8. package/dist/server/_impl.d.ts +62 -6
  9. package/dist/server/_impl.d.ts.map +1 -1
  10. package/dist/server/_impl.js +116 -29
  11. package/dist/server/_impl.js.map +1 -1
  12. package/dist/server/services/index.d.ts +2 -0
  13. package/dist/server/services/index.d.ts.map +1 -1
  14. package/dist/server/services/index.js +2 -0
  15. package/dist/server/services/index.js.map +1 -1
  16. package/dist/server/services/publications.d.ts +70 -23
  17. package/dist/server/services/publications.d.ts.map +1 -1
  18. package/dist/server/services/publications.js +286 -8
  19. package/dist/server/services/publications.js.map +1 -1
  20. package/dist/server/services/versions.d.ts +22 -1
  21. package/dist/server/services/versions.d.ts.map +1 -1
  22. package/dist/server/services/versions.js +44 -15
  23. package/dist/server/services/versions.js.map +1 -1
  24. package/migrations/0009_public_asset_references.sql +35 -0
  25. package/migrations/meta/_journal.json +7 -0
  26. package/package.json +1 -1
  27. package/src/client/components/PublishDialog.svelte +75 -12
  28. package/src/client/components/SidebarRecents.svelte +17 -1
  29. package/src/client/i18n.ts +14 -0
  30. package/src/client/mock.ts +10 -0
  31. package/src/client/pages/PageView.svelte +11 -11
  32. package/src/client/public-url.ts +25 -13
  33. package/src/contract/permissions.ts +1 -0
  34. package/src/contract/router.ts +100 -6
@@ -4942,7 +4942,7 @@ export declare function implement_(kernel: Kernel): {
4942
4942
  headers: Record<string, string | string[] | undefined>;
4943
4943
  workspaceId?: string;
4944
4944
  }, import("zod").ZodObject<{
4945
- workspaceId: import("zod/v4/core").$ZodBranded<import("zod").ZodUUID, "WorkspaceId", "out">;
4945
+ workspaceId: import("zod").ZodString;
4946
4946
  slug: import("zod").ZodString;
4947
4947
  token: import("zod").ZodDefault<import("zod").ZodNullable<import("zod").ZodString>>;
4948
4948
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
@@ -5014,7 +5014,7 @@ export declare function implement_(kernel: Kernel): {
5014
5014
  headers: Record<string, string | string[] | undefined>;
5015
5015
  workspaceId?: string;
5016
5016
  }, import("zod").ZodObject<{
5017
- workspaceId: import("zod/v4/core").$ZodBranded<import("zod").ZodUUID, "WorkspaceId", "out">;
5017
+ workspaceId: import("zod").ZodString;
5018
5018
  slug: import("zod").ZodString;
5019
5019
  path: import("zod").ZodDefault<import("zod").ZodString>;
5020
5020
  basePath: import("zod").ZodDefault<import("zod").ZodString>;
@@ -5079,7 +5079,7 @@ export declare function implement_(kernel: Kernel): {
5079
5079
  headers: Record<string, string | string[] | undefined>;
5080
5080
  workspaceId?: string;
5081
5081
  }, import("zod").ZodObject<{
5082
- workspaceId: import("zod/v4/core").$ZodBranded<import("zod").ZodUUID, "WorkspaceId", "out">;
5082
+ workspaceId: import("zod").ZodString;
5083
5083
  slug: import("zod").ZodString;
5084
5084
  q: import("zod").ZodString;
5085
5085
  limit: import("zod").ZodDefault<import("zod").ZodNumber>;
@@ -5138,7 +5138,7 @@ export declare function implement_(kernel: Kernel): {
5138
5138
  headers: Record<string, string | string[] | undefined>;
5139
5139
  workspaceId?: string;
5140
5140
  }, import("zod").ZodObject<{
5141
- workspaceId: import("zod/v4/core").$ZodBranded<import("zod").ZodUUID, "WorkspaceId", "out">;
5141
+ workspaceId: import("zod").ZodString;
5142
5142
  slug: import("zod").ZodString;
5143
5143
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
5144
5144
  entries: import("zod").ZodArray<import("zod").ZodObject<{
@@ -5193,7 +5193,7 @@ export declare function implement_(kernel: Kernel): {
5193
5193
  headers: Record<string, string | string[] | undefined>;
5194
5194
  workspaceId?: string;
5195
5195
  }, import("zod").ZodObject<{
5196
- workspaceId: import("zod/v4/core").$ZodBranded<import("zod").ZodUUID, "WorkspaceId", "out">;
5196
+ workspaceId: import("zod").ZodString;
5197
5197
  slug: import("zod").ZodString;
5198
5198
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
5199
5199
  indexable: import("zod").ZodBoolean;
@@ -5221,6 +5221,62 @@ export declare function implement_(kernel: Kernel): {
5221
5221
  }, import("zod/v4/core").$strip>;
5222
5222
  };
5223
5223
  }>>, Record<never, never>>;
5224
+ asset: import("@orpc/server").Procedure<import("@orpc/server").MergedInitialContext<RequestContext & Record<never, never>, RequestContext & Record<never, never> & Omit<RequestContext & Record<never, never>, keyof RequestContext> & Omit<RequestContext & Record<never, never>, "workspaceId" | "kernel" | "principal" | "requestId" | "ip" | "headers">, RequestContext>, Omit<import("@orpc/server").MergedCurrentContext<RequestContext, Record<never, never>>, "workspaceId" | "kernel" | "principal" | "requestId" | "ip" | "headers"> & {
5225
+ principal: {
5226
+ kind: "service" | "user" | "api_key" | "anonymous";
5227
+ userId: (string & import("zod").$brand<"UserId">) | null;
5228
+ email: string | null;
5229
+ name: string | null;
5230
+ locale: "en" | "fa" | "ar" | "de";
5231
+ instanceAdmin: boolean;
5232
+ service: string | null;
5233
+ memberships: {
5234
+ workspaceId: string & import("zod").$brand<"WorkspaceId">;
5235
+ role: "owner" | "admin" | "member" | "guest";
5236
+ roleIds: string[];
5237
+ groupIds: string[];
5238
+ status: "active" | "invited" | "suspended";
5239
+ }[];
5240
+ permissionVersion: number;
5241
+ apiKeyScope?: "read" | "read_write" | undefined;
5242
+ };
5243
+ kernel: Kernel;
5244
+ requestId: string;
5245
+ ip: string;
5246
+ headers: Record<string, string | string[] | undefined>;
5247
+ workspaceId?: string;
5248
+ }, import("zod").ZodObject<{
5249
+ workspaceId: import("zod").ZodString;
5250
+ slug: import("zod").ZodString;
5251
+ asset: import("zod").ZodString;
5252
+ token: import("zod").ZodDefault<import("zod").ZodNullable<import("zod").ZodString>>;
5253
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
5254
+ contentType: import("zod").ZodString;
5255
+ bytes: import("zod").ZodString;
5256
+ maxAge: import("zod").ZodNumber;
5257
+ }, import("zod/v4/core").$strip>, import("@orpc/server").MergedErrorMap<Record<never, never>, import("@orpc/server").MergedErrorMap<Record<never, never>, {
5258
+ BAD_REQUEST: {
5259
+ data: import("zod").ZodOptional<import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>>;
5260
+ };
5261
+ UNAUTHORIZED: {};
5262
+ FORBIDDEN: {
5263
+ data: import("zod").ZodOptional<import("zod").ZodObject<{
5264
+ permission: import("zod").ZodOptional<import("zod").ZodString>;
5265
+ }, import("zod/v4/core").$strip>>;
5266
+ };
5267
+ NOT_FOUND: {};
5268
+ CONFLICT: {
5269
+ data: import("zod").ZodOptional<import("zod").ZodObject<{
5270
+ reason: import("zod").ZodOptional<import("zod").ZodString>;
5271
+ }, import("zod/v4/core").$strip>>;
5272
+ };
5273
+ RATE_LIMITED: {};
5274
+ MODULE_DISABLED: {
5275
+ data: import("zod").ZodObject<{
5276
+ module: import("zod").ZodString;
5277
+ }, import("zod/v4/core").$strip>;
5278
+ };
5279
+ }>>, Record<never, never>>;
5224
5280
  unlock: import("@orpc/server").Procedure<import("@orpc/server").MergedInitialContext<RequestContext & Record<never, never>, RequestContext & Record<never, never> & Omit<RequestContext & Record<never, never>, keyof RequestContext> & Omit<RequestContext & Record<never, never>, "workspaceId" | "kernel" | "principal" | "requestId" | "ip" | "headers">, RequestContext>, Omit<import("@orpc/server").MergedCurrentContext<RequestContext, Record<never, never>>, "workspaceId" | "kernel" | "principal" | "requestId" | "ip" | "headers"> & {
5225
5281
  principal: {
5226
5282
  kind: "service" | "user" | "api_key" | "anonymous";
@@ -5246,7 +5302,7 @@ export declare function implement_(kernel: Kernel): {
5246
5302
  headers: Record<string, string | string[] | undefined>;
5247
5303
  workspaceId?: string;
5248
5304
  }, import("zod").ZodObject<{
5249
- workspaceId: import("zod/v4/core").$ZodBranded<import("zod").ZodUUID, "WorkspaceId", "out">;
5305
+ workspaceId: import("zod").ZodString;
5250
5306
  slug: import("zod").ZodString;
5251
5307
  password: import("zod").ZodString;
5252
5308
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"_impl.d.ts","sourceRoot":"","sources":["../../src/server/_impl.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,kBAAkB,EAElB,KAAK,MAAM,EAEX,cAAc,EACd,KAAK,cAAc,EAIpB,MAAM,gBAAgB,CAAA;AAWvB;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,CAAA;AAwC3D,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAksCxC"}
1
+ {"version":3,"file":"_impl.d.ts","sourceRoot":"","sources":["../../src/server/_impl.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,kBAAkB,EAElB,KAAK,MAAM,EAEX,cAAc,EACd,KAAK,cAAc,EAIpB,MAAM,gBAAgB,CAAA;AAWvB;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,CAAA;AAqF3D,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2uCxC"}
@@ -3,7 +3,7 @@ import { defineModule, defineServerModule, KernError, o, packageVersion, require
3
3
  import { implement } from '@orpc/server';
4
4
  import { MODULE_ID, quireContract, quireEvents } from '../contract/index.js';
5
5
  import { toComment } from './services/comments.js';
6
- import { quireServices } from './services/index.js';
6
+ import { quireServices, resolveWorkspaceSegment } from './services/index.js';
7
7
  import { createNotify } from './services/notify.js';
8
8
  import { documentNameOf, toPage } from './services/pages.js';
9
9
  import { toPublication } from './services/publications.js';
@@ -28,11 +28,41 @@ const os = implement(quireContract).$context();
28
28
  * a URL is owed "there is nothing here" and not "there is something here that this customer has
29
29
  * switched off". The one thing it must not do is `requireMember`.
30
30
  */
31
+ const UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
31
32
  const publicSurface = o.middleware(async ({ context, next }, input) => {
32
33
  const { workspaceId } = input;
33
- if (typeof workspaceId !== 'string' || workspaceId.length === 0)
34
+ /*
35
+ * A middleware runs *before* the contract validates its input, so this is the first and only
36
+ * thing that sees the workspace segment — and it used to hand whatever arrived to
37
+ * `kernel.isModuleEnabled`, whose own schema is `z.uuid()`. So `/api/quire/public/not-a-uuid/x`
38
+ * came back as a ZodError this module has no case for: **HTTP 500** on the one surface that
39
+ * promises everything unresolvable is a 404, and a stack trace written at error level for every
40
+ * such request, on an endpoint anyone on the internet can reach as fast as they like.
41
+ *
42
+ * The shape is checked here instead. Anything that is not a workspace id is the same "there is
43
+ * nothing here" as a workspace that has switched Quire off — which is the right answer, because a
44
+ * signed-out stranger is owed no more than that. `.catch` covers the rest for the same reason: a
45
+ * settings lookup that fails must not become a 500 either.
46
+ */
47
+ if (typeof workspaceId !== 'string')
48
+ throw new KernError('NOT_FOUND', 'There is no published site at this address');
49
+ /*
50
+ * A public URL names its workspace by **slug**, because the address the share dialog copies is
51
+ * meant to be sent to somebody — and a uuid in a link is a receipt, not an address. This
52
+ * middleware is the first thing that sees the segment, so it is also the first thing that used to
53
+ * refuse it: a `UUID.test` here answered 404 for the module's own published URLs while the same
54
+ * site served perfectly under its id. Resolving happens here so that `isModuleEnabled` below is
55
+ * asked about a workspace rather than about a slug, whose schema is `z.uuid()` and would 500.
56
+ *
57
+ * A slug that names no workspace, and a slug that names one with no such publication, both reach
58
+ * the same 404 as a workspace nobody has — so this resolves an address without answering "does
59
+ * this workspace exist" for anyone who asks.
60
+ */
61
+ const resolved = await resolveWorkspaceSegment(context.kernel, workspaceId).catch(() => null);
62
+ if (!resolved)
34
63
  throw new KernError('NOT_FOUND', 'There is no published site at this address');
35
- if (!(await context.kernel.isModuleEnabled(workspaceId, MODULE_ID)))
64
+ const enabled = await context.kernel.isModuleEnabled(resolved, MODULE_ID).catch(() => false);
65
+ if (!enabled)
36
66
  throw new KernError('NOT_FOUND', 'There is no published site at this address');
37
67
  return next();
38
68
  });
@@ -50,11 +80,44 @@ const publicSurface = o.middleware(async ({ context, next }, input) => {
50
80
  * procedure must answer a denied administrator and an anonymous request identically.
51
81
  */
52
82
  const anonymousOnly = o.middleware(({ context, next }) => next({ context: { ...context, principal: ANONYMOUS } }));
83
+ /**
84
+ * The one refusal every unservable publication shares, in one place so it cannot drift.
85
+ *
86
+ * It drifted. A publication whose root page had since been trashed was 404 from `site` and `page`,
87
+ * **200 with an empty body** from `search` and `sitemap`, and `indexable: true` from `robots` — so
88
+ * four procedures written to be indistinguishable from "no such slug" were, between them, an
89
+ * existence oracle for a fifth state nobody had enumerated. And `page` said "page" where the others
90
+ * said "site", which separated a locked publication from a missing one on the endpoint least able
91
+ * to afford it.
92
+ *
93
+ * The rule the wording now follows: **a refusal about the publication says "site", and a refusal
94
+ * about a path inside a servable one says "page".** A reader who mistypes a page of a real handbook
95
+ * is the only person who ever sees the second sentence.
96
+ */
97
+ const noSite = () => new KernError('NOT_FOUND', 'There is no published site at this address');
53
98
  export function implement_(kernel) {
54
99
  const scoped = os.use(workspaceScoped(MODULE_ID));
55
100
  const open = os.use(publicSurface).use(anonymousOnly);
56
101
  const svc = quireServices(kernel);
57
102
  const notify = createNotify(kernel);
103
+ /**
104
+ * A publication that can actually be served, and its whole tree, or `noSite`.
105
+ *
106
+ * Every anonymous read but `site` and `robots` starts here, so the four ways of not being
107
+ * servable — no such slug, expired, still locked, nothing left under the root — are one answer
108
+ * written once rather than four handlers agreeing by hand. `site` needs its own shape because
109
+ * `locked` is a state it reports rather than refuses, and `robots` because it never refuses at
110
+ * all.
111
+ */
112
+ const servable = async (tx, workspaceId, slug, token) => {
113
+ const pub = await svc.publications.bySlug(tx, workspaceId, slug);
114
+ if (!(await svc.publications.unlocked(pub, token)))
115
+ throw noSite();
116
+ const nodes = await svc.publications.tree(tx, workspaceId, pub);
117
+ if (!nodes[0])
118
+ throw noSite();
119
+ return { pub, nodes };
120
+ };
58
121
  const run = (context, workspaceId, fn) => kernel.database.withWorkspace(workspaceId, fn, { userId: context.principal.userId });
59
122
  /**
60
123
  * "May you do this to *that* page", answered through the page's ancestor chain.
@@ -834,17 +897,17 @@ export function implement_(kernel) {
834
897
  * transaction, so a write that slipped in here fails in Postgres rather than in a review.
835
898
  */
836
899
  public: {
837
- site: open.public.site.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx) => {
838
- const pub = await svc.publications.bySlug(tx, input.workspaceId, input.slug);
900
+ site: open.public.site.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
901
+ const pub = await svc.publications.bySlug(tx, ws, input.slug);
839
902
  const theme = pub.theme;
840
903
  if (!(await svc.publications.unlocked(pub, input.token)))
841
904
  return { slug: pub.slug, theme, locked: true, site: null };
842
- const nodes = await svc.publications.tree(tx, input.workspaceId, pub);
905
+ const nodes = await svc.publications.tree(tx, ws, pub);
843
906
  // No root means the root page is archived, trashed, opted out, unpublished or not a page.
844
907
  // The publication row survives that; the site does not.
845
908
  const root = nodes[0];
846
909
  if (!root)
847
- throw new KernError('NOT_FOUND', 'There is no published site at this address');
910
+ throw noSite();
848
911
  const newest = nodes.reduce((at, node) => Math.max(at, node.published_at.getTime()), root.published_at.getTime());
849
912
  return {
850
913
  slug: pub.slug,
@@ -865,18 +928,15 @@ export function implement_(kernel) {
865
928
  },
866
929
  };
867
930
  })),
868
- page: open.public.page.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx) => {
869
- const pub = await svc.publications.bySlug(tx, input.workspaceId, input.slug);
870
- if (!(await svc.publications.unlocked(pub, input.token)))
871
- throw new KernError('NOT_FOUND', 'There is no published page at this address');
872
- const nodes = await svc.publications.tree(tx, input.workspaceId, pub);
931
+ page: open.public.page.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
932
+ const { nodes } = await servable(tx, ws, input.slug, input.token);
873
933
  const node = svc.publications.find(nodes, input.path);
874
934
  return {
875
935
  path: node.path,
876
936
  title: node.title || 'Untitled',
877
937
  icon: node.icon,
878
938
  coverUrl: node.cover_url,
879
- html: await svc.publications.html(tx, input.workspaceId, node, nodes, input.basePath),
939
+ html: await svc.publications.html(tx, ws, node, nodes, input.basePath),
880
940
  /*
881
941
  * The version's timestamp, not the page's. `pages.updated_at` moves every time somebody
882
942
  * types in the draft, so publishing it would tell the internet when an unpublished
@@ -887,12 +947,9 @@ export function implement_(kernel) {
887
947
  breadcrumbs: trailTo(nodes, node),
888
948
  };
889
949
  })),
890
- search: open.public.search.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx) => {
891
- const pub = await svc.publications.bySlug(tx, input.workspaceId, input.slug);
892
- if (!(await svc.publications.unlocked(pub, input.token)))
893
- throw new KernError('NOT_FOUND', 'There is no published site at this address');
894
- const nodes = await svc.publications.tree(tx, input.workspaceId, pub);
895
- const hits = await svc.publications.search(tx, input.workspaceId, nodes, input.q, input.limit);
950
+ search: open.public.search.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
951
+ const { nodes } = await servable(tx, ws, input.slug, input.token);
952
+ const hits = await svc.publications.search(tx, ws, nodes, input.q, input.limit);
896
953
  return {
897
954
  items: hits.map((hit) => ({
898
955
  path: hit.node.path,
@@ -901,14 +958,19 @@ export function implement_(kernel) {
901
958
  })),
902
959
  };
903
960
  })),
904
- sitemap: open.public.sitemap.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx) => {
905
- const pub = await svc.publications.bySlug(tx, input.workspaceId, input.slug);
961
+ sitemap: open.public.sitemap.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
962
+ const pub = await svc.publications.bySlug(tx, ws, input.slug);
906
963
  // A sitemap exists for crawlers, so a site nobody is meant to crawl has an empty one
907
964
  // rather than a private one. Answering with the tree here would put every path of a
908
965
  // password-protected handbook in a file whose whole purpose is to be fetched by robots.
909
966
  if (pub.passwordHash || !pub.indexable)
910
967
  return { entries: [] };
911
- const nodes = await svc.publications.tree(tx, input.workspaceId, pub);
968
+ const nodes = await svc.publications.tree(tx, ws, pub);
969
+ // A publication whose root has since been trashed serves nothing, so it is the same 404
970
+ // as a slug nobody took — an empty sitemap here would say "this address is a site" to a
971
+ // crawler that has just been told the opposite by every other procedure.
972
+ if (!nodes[0])
973
+ throw noSite();
912
974
  return {
913
975
  entries: nodes.map((node) => ({
914
976
  path: node.path,
@@ -916,19 +978,44 @@ export function implement_(kernel) {
916
978
  })),
917
979
  };
918
980
  })),
919
- robots: open.public.robots.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx) => {
920
- // The one place a missing publication is not an error: a crawler asking about a slug
921
- // nobody has taken, one that has expired, and one behind a password have to get the same
922
- // answer, or this becomes the oracle every other procedure refuses to be.
923
- const pub = await svc.publications.bySlug(tx, input.workspaceId, input.slug).catch(() => null);
981
+ robots: open.public.robots.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
982
+ /*
983
+ * The one place a missing publication is not an error: a crawler asking about a slug
984
+ * nobody has taken, one that has expired, and one behind a password have to get the same
985
+ * answer, or this becomes the oracle every other procedure refuses to be.
986
+ *
987
+ * There is a **fourth** state and this comment used to list three. A publication whose
988
+ * root page has since been trashed, archived, opted out or unpublished still has its row:
989
+ * `site`, `page`, `search` and `sitemap` all answer 404 for it, and robots answered
990
+ * `indexable: true` with a sitemap path — so the one procedure written to never
991
+ * distinguish anything was the only one admitting that the slug existed. It is the walk
992
+ * rather than the row that decides, which is why the tree is read here.
993
+ */
994
+ const pub = await svc.publications.bySlug(tx, ws, input.slug).catch(() => null);
924
995
  if (!pub || pub.passwordHash || !pub.indexable)
925
996
  return { indexable: false, sitemapPath: null };
997
+ const nodes = await svc.publications.tree(tx, ws, pub);
998
+ if (!nodes[0])
999
+ return { indexable: false, sitemapPath: null };
926
1000
  // Relative to whatever prefix the route layer serves this site under; the module has no
927
1001
  // way to know that, and guessing would put a wrong absolute URL in a robots file.
928
1002
  return { indexable: true, sitemapPath: 'sitemap.xml' };
929
1003
  })),
930
- unlock: open.public.unlock.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx) => {
931
- const pub = await svc.publications.bySlug(tx, input.workspaceId, input.slug);
1004
+ /**
1005
+ * The bytes of one picture on a published page.
1006
+ *
1007
+ * It goes through `servable` like every other read, so a picture is behind the same door, the
1008
+ * same expiry and the same walk as the prose it sits in — and `svc.publications.asset` then
1009
+ * insists the file is referenced by a version *in this tree* before it fetches anything. That
1010
+ * second check is what stops a reference lifted out of one published site from resolving
1011
+ * against another in the same workspace.
1012
+ */
1013
+ asset: open.public.asset.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
1014
+ const { nodes } = await servable(tx, ws, input.slug, input.token);
1015
+ return svc.publications.asset(tx, ws, nodes, input.asset);
1016
+ })),
1017
+ unlock: open.public.unlock.handler(({ input }) => svc.publications.read(input.workspaceId, async (tx, ws) => {
1018
+ const pub = await svc.publications.bySlug(tx, ws, input.slug);
932
1019
  // A site with no password has no door to open, and saying so would confirm the slug
933
1020
  // exists to somebody who has not been told anything else about it.
934
1021
  if (!pub.passwordHash)