@supersuit/artifacts 0.58.1 → 0.59.0

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 (70) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +11 -0
  3. package/lib/artifacts/letter-breaks.d.ts +2 -0
  4. package/lib/artifacts/letter-breaks.js +68 -0
  5. package/lib/artifacts/render.js +2 -1
  6. package/lib/routes/artifacts.js +2 -1
  7. package/lib/routes/backend-auth.d.ts +9 -0
  8. package/lib/routes/backend-auth.js +42 -0
  9. package/package.json +5 -2
  10. package/template/hub/README.md +35 -0
  11. package/template/hub/app/[id]/not-found.tsx +8 -0
  12. package/template/hub/app/[id]/page.tsx +15 -0
  13. package/template/hub/app/[id]/share.png/route.tsx +10 -0
  14. package/template/hub/app/[id]/v/[n]/page.tsx +15 -0
  15. package/template/hub/app/api/artifact-pass/route.ts +24 -0
  16. package/template/hub/app/api/artifacts/[id]/access/route.ts +14 -0
  17. package/template/hub/app/api/artifacts/[id]/assets/[name]/route.ts +10 -0
  18. package/template/hub/app/api/artifacts/[id]/forward/route.ts +10 -0
  19. package/template/hub/app/api/artifacts/[id]/personal/route.ts +18 -0
  20. package/template/hub/app/api/artifacts/[id]/reads/route.ts +10 -0
  21. package/template/hub/app/api/artifacts/[id]/recipients/route.ts +14 -0
  22. package/template/hub/app/api/artifacts/[id]/reply/route.ts +10 -0
  23. package/template/hub/app/api/artifacts/[id]/resolve/route.ts +10 -0
  24. package/template/hub/app/api/artifacts/[id]/responses/route.ts +14 -0
  25. package/template/hub/app/api/artifacts/[id]/route.ts +12 -0
  26. package/template/hub/app/api/artifacts/[id]/seen/route.ts +10 -0
  27. package/template/hub/app/api/artifacts/[id]/settings/route.ts +14 -0
  28. package/template/hub/app/api/artifacts/[id]/state/route.ts +14 -0
  29. package/template/hub/app/api/artifacts/[id]/transcribe/route.ts +10 -0
  30. package/template/hub/app/api/artifacts/[id]/uploads/[name]/route.ts +11 -0
  31. package/template/hub/app/api/artifacts/[id]/versions/route.ts +14 -0
  32. package/template/hub/app/api/artifacts/[id]/views/route.ts +10 -0
  33. package/template/hub/app/api/artifacts/route.ts +13 -0
  34. package/template/hub/app/api/comments/route.ts +8 -0
  35. package/template/hub/app/api/health/route.ts +10 -0
  36. package/template/hub/app/api/reader/ack/route.ts +6 -0
  37. package/template/hub/app/api/reader/enter/route.ts +8 -0
  38. package/template/hub/app/api/reader/leave/route.ts +8 -0
  39. package/template/hub/app/api/reader/track/route.ts +6 -0
  40. package/template/hub/app/artifact/sign-in/page.tsx +29 -0
  41. package/template/hub/app/artifact/sign-in/sign-in.tsx +60 -0
  42. package/template/hub/app/brand.json/route.ts +10 -0
  43. package/template/hub/app/frapp/font/route.ts +12 -0
  44. package/template/hub/app/frapp/og/route.ts +17 -0
  45. package/template/hub/app/frapp/route.ts +28 -0
  46. package/template/hub/app/globals.css +7 -0
  47. package/template/hub/app/layout.tsx +32 -0
  48. package/template/hub/app/page.tsx +26 -0
  49. package/template/hub/app/robots.ts +6 -0
  50. package/template/hub/apphosting.yaml +21 -0
  51. package/template/hub/firestore.indexes.json +13 -0
  52. package/template/hub/next.config.ts +27 -0
  53. package/template/hub/package.json +34 -0
  54. package/template/hub/postcss.config.mjs +2 -0
  55. package/template/hub/proxy.ts +24 -0
  56. package/template/hub/src/lib/brand-colors.ts +13 -0
  57. package/template/hub/src/lib/firebase-admin.ts +46 -0
  58. package/template/hub/src/lib/frapp-door-theme.ts +56 -0
  59. package/template/hub/src/lib/frapp-door.d.mts +24 -0
  60. package/template/hub/src/lib/frapp-door.mjs +321 -0
  61. package/template/hub/src/lib/health.ts +23 -0
  62. package/template/hub/src/lib/hub-env-parse.ts +90 -0
  63. package/template/hub/src/lib/hub-env.ts +10 -0
  64. package/template/hub/src/lib/hub-meta.ts +12 -0
  65. package/template/hub/src/lib/local-sign-in.ts +46 -0
  66. package/template/hub/src/lib/routes.ts +44 -0
  67. package/template/hub/src/lib/url-mark.tsx +9 -0
  68. package/template/hub/src/tenant.ts +60 -0
  69. package/template/hub/tailwind.config.ts +8 -0
  70. package/template/hub/tsconfig.json +21 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,51 @@
3
3
  `@supersuit/artifacts`. One entry per version, newest first. Each entry says what changed, how a
4
4
  host can tell whether it is affected (DETECTOR), what a host does about it (REMEDY), and the tests.
5
5
 
6
+ ## 0.59.0 (2026-11-02)
7
+
8
+ **The deployable hub host ships in the package** (supersuit-dev#1630). `template/hub/` is a complete
9
+ Next.js app for one operator's own artifact hub on Firebase App Hosting: every shell route as a
10
+ one-line file, Google sign-in on the hub's own domain (the same-origin `/__/auth` rewrite and
11
+ `/api/artifact-pass`), `/brand.json`, `/api/health` with the tile app door's version, the door
12
+ itself at `/frapp`, the 410 proxy for shut per-reader links, `apphosting.yaml` with every setting the
13
+ hub setup writes, and the comments feed's Firestore index. It is configured only by `HUB_TENANT`
14
+ and the variables in its README, so it needs nothing from any private repository. Until now the
15
+ only deployable host was private, and an operator's hub setup stopped at its first step with no
16
+ source they could reach (supersuit-dev#1606). `npm version` pins the template to the package's own
17
+ version (`scripts/sync-template-version.mjs`).
18
+
19
+ - DETECTOR: `ls node_modules/@supersuit/artifacts/template/hub` is empty or missing.
20
+ - REMEDY: update. Supersuit's hub setup uses this host when `--hub-source` is not given.
21
+ - Tests: `test/hub-template.test.mjs`; `npm run test:template` (`scripts/check-template.mjs`)
22
+ installs the template from the packed tarball outside the repo and runs `next build`, and the
23
+ publish workflow runs it before publishing.
24
+
25
+ ## 0.58.2 (2026-11-02)
26
+
27
+ **A letter's greeting and sign-off keep their own lines.** Markdown joins a soft line break with a
28
+ space, so a letter ending "Your friend in Christ," then the name on a plain newline drew as one
29
+ line. `remarkLetterBreaks` (in `ArtifactMarkdown`'s plugin list) turns a paragraph that opens on a
30
+ short line (40 characters or fewer) ending in a comma into real breaks after each short line in it.
31
+ Hard-wrapped prose still joins.
32
+
33
+ - DETECTOR: a page whose sign-off reads "Love, Gary" on one line.
34
+ - REMEDY: update; nothing to change in the page.
35
+ - Tests: `src/artifacts/letter-breaks.test.tsx`.
36
+
37
+ **A host that cannot sign in to its own database says so, instead of a bare 500.** When a host's
38
+ cloud credential is refused (a workload identity provider trusting another Vercel team's OIDC
39
+ issuer, or a revoked service account key), every Firestore call threw and every route answered an
40
+ empty 500. Publishers saw "500, no message", and the Supersuit comments client guessed "a missing
41
+ index" (2026-10-28, after a hub's Vercel project moved teams). Every route handler in
42
+ `createArtifactRoutes` now answers that one cause with `503 { error: "backend-auth", message }`. The
43
+ cause itself is logged on the server and kept out of the body. Any other error is unchanged.
44
+
45
+ - DETECTOR: the server log shows `invalid_grant`, `Getting metadata from plugin failed`,
46
+ `UNAUTHENTICATED` or `Could not load the default credentials`; clients now get a typed 503.
47
+ - REMEDY: fix the host's credential. For Vercel workload identity, point the provider's issuer at
48
+ `https://oidc.vercel.com/<team>` and its audience at `https://vercel.com/<team>`.
49
+ - Tests: `src/routes/backend-auth.test.tsx`.
50
+
6
51
  ## 0.58.1 (2026-10-27)
7
52
 
8
53
  **The no-link door on a per-reader page offers sign-in.** A signed-in owner or hub admin already got
package/README.md CHANGED
@@ -15,6 +15,17 @@ npm install @supersuit/artifacts
15
15
  Peers: `next >= 16`, `react >= 19`, `react-dom >= 19`, `firebase-admin >= 13` (the store is
16
16
  Firestore), and optionally `@google-cloud/storage >= 7` for uploaded files.
17
17
 
18
+ ## A deployable hub host, in the package
19
+
20
+ `template/hub/` is a complete Next.js app that serves one operator's pages from a Google Cloud
21
+ project they own (Firebase App Hosting, Firestore, Cloud Storage, Google sign-in on their own
22
+ domain), configured entirely by environment variables. It ships in the tarball so anyone can
23
+ deploy a hub without access to any private repository. Supersuit's `create-or-update-my-artifact-hub`
24
+ copies it out of the package, runs `npm install` and deploys it; its own
25
+ [README](./template/hub/README.md) lists every variable. The template pins this package's own
26
+ version (`npm version` keeps it in step), and `npm run test:template` packs the package, installs
27
+ the template from the tarball outside this repo and runs `next build` on it.
28
+
18
29
  ## What you get
19
30
 
20
31
  | Export | What it is |
@@ -0,0 +1,2 @@
1
+ import type { Root } from 'mdast';
2
+ export declare function remarkLetterBreaks(): (tree: Root) => void;
@@ -0,0 +1,68 @@
1
+ const SHORT = 40;
2
+ const isParent = (n) => !!n && Array.isArray(n.children);
3
+ /** The paragraph's text as the writer typed it, soft breaks included. */
4
+ function plain(nodes) {
5
+ return nodes.map((n) => {
6
+ const x = n;
7
+ if (x.type === 'text' || x.type === 'inlineCode')
8
+ return x.value ?? '';
9
+ if (x.type === 'break')
10
+ return '\n';
11
+ return x.children ? plain(x.children) : '';
12
+ }).join('');
13
+ }
14
+ function breakLetter(p) {
15
+ const lines = plain(p.children).split('\n');
16
+ if (lines.length < 2)
17
+ return;
18
+ const short = (l) => l.trim().length <= SHORT;
19
+ if (!short(lines[0]) || !/,$/.test(lines[0].trim()))
20
+ return;
21
+ let line = 0;
22
+ const walk = (parent) => {
23
+ const out = [];
24
+ for (const child of parent.children) {
25
+ const c = child;
26
+ if (c.type === 'text') {
27
+ const parts = c.value.split('\n');
28
+ parts.forEach((part, k) => {
29
+ if (k > 0) {
30
+ const keep = short(lines[line]);
31
+ line++;
32
+ if (keep) {
33
+ out.push({ type: 'break' });
34
+ part = part.replace(/^\s+/, '');
35
+ }
36
+ else
37
+ part = ' ' + part.replace(/^\s+/, '');
38
+ }
39
+ if (part)
40
+ out.push({ type: 'text', value: part });
41
+ });
42
+ continue;
43
+ }
44
+ if (c.type === 'break')
45
+ line++;
46
+ else if (isParent(c))
47
+ walk(c);
48
+ out.push(c);
49
+ }
50
+ parent.children = out;
51
+ };
52
+ walk(p);
53
+ }
54
+ export function remarkLetterBreaks() {
55
+ return (tree) => {
56
+ const visit = (n) => {
57
+ if (!isParent(n))
58
+ return;
59
+ for (const c of n.children) {
60
+ if (c.type === 'paragraph')
61
+ breakLetter(c);
62
+ else
63
+ visit(c);
64
+ }
65
+ };
66
+ visit(tree);
67
+ };
68
+ }
@@ -23,6 +23,7 @@ import { VideoAutoplay } from '../reader/video-autoplay.js';
23
23
  import { CodeCopy } from '../reader/code-copy.js';
24
24
  import { DefinitionLayer } from '../reader/definition-layer.js';
25
25
  import { remarkDefinitions } from './definitions.js';
26
+ import { remarkLetterBreaks } from './letter-breaks.js';
26
27
  import { AUDIO_SRC, remarkVoiceNotes, TRANSCRIPT_LANG } from './voice-note.js';
27
28
  import { VoiceNote } from '../widgets/voice-note.js';
28
29
  // Every colour is a theme variable (brand/theme.ts), so a page follows its light, dark or system mode.
@@ -371,7 +372,7 @@ export function ArtifactMarkdown({ markdown, notes, checklist, definitions, Sect
371
372
  // Voice notes first, so a section groups the note node the pair became.
372
373
  const voiceNotes = [];
373
374
  const voiced = /^ {0,3}(`{3,}|~{3,})\s*transcript\b/m.test(markdown);
374
- const plugins = [remarkGfm, ...(voiced ? [remarkVoiceNotes(voiceNotes)] : []), ...(defined ? [remarkDefinitions(defined)] : []), ...(outline.length ? [remarkSections(outline[0].depth)] : [])];
375
+ const plugins = [remarkGfm, remarkLetterBreaks, ...(voiced ? [remarkVoiceNotes(voiceNotes)] : []), ...(defined ? [remarkDefinitions(defined)] : []), ...(outline.length ? [remarkSections(outline[0].depth)] : [])];
375
376
  const lists = /^ {0,3}(`{3,}|~{3,})\s*checklist\b/m.test(markdown) ? checklistsOf(markdown) : null;
376
377
  const checklists = lists
377
378
  // `send` from the host means it keeps answers and the page declared the `checklist` slot; on a
@@ -40,6 +40,7 @@ import { PageAnalytics } from '../reader/page-analytics.js';
40
40
  import { analyticsPlan } from '../reader/analytics-plan.js';
41
41
  import { createPersonalRoutes } from './personal-routes.js';
42
42
  import { createVoiceRoutes } from './voice-routes.js';
43
+ import { guardRouteTable } from './backend-auth.js';
43
44
  /** Ids are 8 chars from the safe alphabet; anything else is not a page and never reaches the store. */
44
45
  export const ARTIFACT_ID = ARTIFACT_ID_RE;
45
46
  /** A version number as it appears in a URL: no zero, no leading zero, so each version has one address. */
@@ -1124,5 +1125,5 @@ export function createArtifactRoutes(config) {
1124
1125
  + `<p data-per-reader-shut style="max-width:36rem;padding:0 1.5rem;text-align:center;font-size:1.125rem;opacity:.8">${UNAVAILABLE}</p></body></html>`;
1125
1126
  return new Response(html, { status: 410, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-robots-tag': 'noindex, nofollow' } });
1126
1127
  }
1127
- return { Page, generateMetadata, VersionPage, generateVersionMetadata, VERSIONS, POST, GET, DELETE, LIST, shutPageResponse, PUT_ASSET, UPLOAD, SHARE_IMAGE, ENTER, LEAVE, TRACK, ACK, ACCESS, SETTINGS, READS, sweepReaders, TRANSCRIBE: voiceRoutes.TRANSCRIBE, ...stateRoutes, ...personalRoutes, ...recipientsRoutes, dynamic: 'force-dynamic', maxDuration: 30 };
1128
+ return guardRouteTable({ Page, generateMetadata, VersionPage, generateVersionMetadata, VERSIONS, POST, GET, DELETE, LIST, shutPageResponse, PUT_ASSET, UPLOAD, SHARE_IMAGE, ENTER, LEAVE, TRACK, ACK, ACCESS, SETTINGS, READS, sweepReaders, TRANSCRIBE: voiceRoutes.TRANSCRIBE, ...stateRoutes, ...personalRoutes, ...recipientsRoutes, dynamic: 'force-dynamic', maxDuration: 30 });
1128
1129
  }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from 'next/server';
2
+ /** True when an error is the host failing to authenticate to its own backend. */
3
+ export declare function isBackendAuthError(e: unknown): boolean;
4
+ export declare const BACKEND_AUTH_MESSAGE = "This host could not sign in to its database, so nothing was read or saved. Its owner should check the host's cloud credentials (the workload identity provider's issuer and audience, or the service account key).";
5
+ export declare function backendAuthResponse(): NextResponse;
6
+ /** Wrap a route handler so a backend credential failure answers the typed 503. */
7
+ export declare function guardBackendAuth<A extends unknown[], R>(handler: (...args: A) => R | Promise<R>): (...args: A) => Promise<R | NextResponse>;
8
+ /** Every route handler in a route table (its UPPER_CASE functions) gets the guard. */
9
+ export declare function guardRouteTable<T extends Record<string, unknown>>(table: T): T;
@@ -0,0 +1,42 @@
1
+ // A host whose cloud credential is refused (a workload identity provider trusting another
2
+ // issuer, a revoked or missing service account key) fails every Firestore call. Unhandled, each
3
+ // route then answers a bare 500 that a publisher reads as "something broke" and a client hint
4
+ // can only guess at (on 2026-10-28 it guessed "a missing index"). This turns that one cause into
5
+ // a typed 503 the publisher can name, and leaves every other error exactly as it was.
6
+ import { NextResponse } from 'next/server';
7
+ const AUTH_MESSAGE = /invalid_grant|Getting metadata from plugin failed|Could not load the default credentials|UNAUTHENTICATED|Request had invalid authentication credentials/i;
8
+ /** True when an error is the host failing to authenticate to its own backend. */
9
+ export function isBackendAuthError(e) {
10
+ if (!(e instanceof Error))
11
+ return false;
12
+ if (e.code === 16)
13
+ return true; // gRPC UNAUTHENTICATED
14
+ return AUTH_MESSAGE.test(e.message);
15
+ }
16
+ export const BACKEND_AUTH_MESSAGE = "This host could not sign in to its database, so nothing was read or saved. Its owner should check the host's cloud credentials (the workload identity provider's issuer and audience, or the service account key).";
17
+ export function backendAuthResponse() {
18
+ return NextResponse.json({ error: 'backend-auth', message: BACKEND_AUTH_MESSAGE }, { status: 503, headers: { 'cache-control': 'no-store' } });
19
+ }
20
+ /** Wrap a route handler so a backend credential failure answers the typed 503. */
21
+ export function guardBackendAuth(handler) {
22
+ return async (...args) => {
23
+ try {
24
+ return await handler(...args);
25
+ }
26
+ catch (e) {
27
+ if (!isBackendAuthError(e))
28
+ throw e;
29
+ console.error('[artifacts] backend credential refused:', e);
30
+ return backendAuthResponse();
31
+ }
32
+ };
33
+ }
34
+ /** Every route handler in a route table (its UPPER_CASE functions) gets the guard. */
35
+ export function guardRouteTable(table) {
36
+ const out = { ...table };
37
+ for (const [k, v] of Object.entries(table)) {
38
+ if (typeof v === 'function' && /^[A-Z][A-Z0-9_]*$/.test(k))
39
+ out[k] = guardBackendAuth(v);
40
+ }
41
+ return out;
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/artifacts",
3
- "version": "0.58.1",
3
+ "version": "0.59.0",
4
4
  "description": "Publish markdown as branded, read-aloud pages from a Next.js app: the artifacts store, the route factory, the read-along reader, brand packs, password and confidential pages, and share cards.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,6 +54,7 @@
54
54
  "files": [
55
55
  "lib",
56
56
  "fonts",
57
+ "template",
57
58
  "README.md",
58
59
  "CHANGELOG.md",
59
60
  "LICENSE"
@@ -64,7 +65,9 @@
64
65
  "typecheck": "tsc -p tsconfig.json --noEmit",
65
66
  "test": "vitest run",
66
67
  "test:packed": "node scripts/check-packed.mjs",
67
- "prepack": "npm run build"
68
+ "prepack": "npm run build",
69
+ "test:template": "node scripts/check-template.mjs",
70
+ "version": "node scripts/sync-template-version.mjs"
68
71
  },
69
72
  "peerDependencies": {
70
73
  "@google-cloud/storage": ">=7",
@@ -0,0 +1,35 @@
1
+ # The artifact hub host
2
+
3
+ A deployable Next.js app that serves one operator's published pages on their own domain, from a
4
+ Google Cloud project they own. It is the `@supersuit/artifacts` shell wired to Firestore, Cloud
5
+ Storage and Firebase sign-in, and nothing else. It ships inside the `@supersuit/artifacts` npm
6
+ package at `template/hub/`.
7
+
8
+ **You do not deploy this by hand.** Supersuit's `create-or-update-my-artifact-hub` skill copies this
9
+ folder out of the installed package, runs `npm install`, and deploys it to Firebase App Hosting with
10
+ the settings it writes for that hub. It reads `apphosting.yaml` for the shape of the settings.
11
+
12
+ ## What configures it
13
+
14
+ Everything comes from the environment. `apphosting.yaml` lists every variable; the setup writes the
15
+ real values into an `apphosting.hub.yaml` override at deploy time.
16
+
17
+ | Variable | What it is |
18
+ | --- | --- |
19
+ | `HUB_TENANT` | JSON: `{ host, owner, firebase: { apiKey, appId, authDomain, projectId }, pack? }`. `authDomain` must equal `host`. `pack` is the hub's own colors, type and mark. |
20
+ | `FIREBASE_STORAGE_BUCKET` | The project's default bucket: public page media. |
21
+ | `ARTIFACT_PRIVATE_BUCKET` | A bucket with no public grant, for `media: private`. Unset, private media is refused. |
22
+ | `TENANT_PUBLISH_KEY_HUB` | The publish key (a Secret Manager secret). |
23
+ | `ARTIFACT_PASS_SECRET` | Signs the five-minute reader pass (a secret). |
24
+ | `ARTIFACT_RECIPIENT_SECRET` | Signs per-reader links (a secret). |
25
+ | `TENANT_OWNER_EMAIL_HUB` | The address the owner signs in with. Optional. |
26
+ | `TENANT_ADMINS_HUB` | Comma-separated admin sign-in addresses. Optional. |
27
+
28
+ With `HUB_TENANT` unset the app still builds, and every page says the hub is not configured yet.
29
+
30
+ ## What it serves
31
+
32
+ Pages at `/<id>`, past versions at `/<id>/v/<n>`, share cards at `/<id>/share.png`, the publish and
33
+ reader APIs under `/api/`, reader sign-in at `/artifact/sign-in` (Google, against the hub's own
34
+ Firebase project, same-origin through the `/__/auth` rewrite), `/brand.json`, `/api/health`, and the
35
+ tile app door at `/frapp`.
@@ -0,0 +1,8 @@
1
+ export default function ArtifactNotFound() {
2
+ return (
3
+ <div className="mx-auto max-w-2xl px-6 pt-32 text-center">
4
+ <h1 className="text-3xl">Nothing here</h1>
5
+ <p className="mt-4 opacity-70">This page was unpublished or the link is off by a character.</p>
6
+ </div>
7
+ )
8
+ }
@@ -0,0 +1,15 @@
1
+ // Published pages: /<id>. Thin file; the behaviour is the shell's, the wiring is src/lib/routes.ts.
2
+ import type { Metadata } from 'next'
3
+ import { hubRoutes } from '@/lib/routes'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ type Props = { params: Promise<{ id: string }>; searchParams?: Promise<{ key?: string | string[] }> }
8
+
9
+ export function generateMetadata(props: Props): Promise<Metadata> {
10
+ return hubRoutes().generateMetadata(props)
11
+ }
12
+
13
+ export default function ArtifactPage(props: Props) {
14
+ return hubRoutes().Page(props)
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().SHARE_IMAGE(request, ctx)
10
+ }
@@ -0,0 +1,15 @@
1
+ // One past version of a page: /<id>/v/<n>, read-only, behind the page's own door.
2
+ import type { Metadata } from 'next'
3
+ import { hubRoutes } from '@/lib/routes'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ type Props = { params: Promise<{ id: string; n: string }>; searchParams?: Promise<{ key?: string | string[] }> }
8
+
9
+ export function generateMetadata(props: Props): Promise<Metadata> {
10
+ return hubRoutes().generateVersionMetadata(props)
11
+ }
12
+
13
+ export default function ArtifactVersionPage(props: Props) {
14
+ return hubRoutes().VersionPage(props)
15
+ }
@@ -0,0 +1,24 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { getAdminAuth } from '@/lib/firebase-admin'
3
+ import { localPass } from '@/lib/local-sign-in'
4
+ import { hubTenant } from '@/tenant'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+ export const runtime = 'nodejs'
8
+
9
+ // The pass for a reader who signed in on this host (app/artifact/sign-in).
10
+ export async function GET(request: NextRequest) {
11
+ const r = await localPass({
12
+ tenant: hubTenant(),
13
+ to: request.nextUrl.searchParams.get('to'),
14
+ authorization: request.headers.get('authorization'),
15
+ verify: async (t) => {
16
+ const d = await getAdminAuth().verifyIdToken(t)
17
+ return { uid: d.uid, email: d.email, emailVerified: d.email_verified === true, name: typeof d.name === 'string' ? d.name : undefined }
18
+ },
19
+ secret: process.env.ARTIFACT_PASS_SECRET,
20
+ })
21
+ return r.ok
22
+ ? Response.json({ url: r.url }, { headers: { 'cache-control': 'no-store' } })
23
+ : Response.json({ error: r.error }, { status: r.status, headers: { 'cache-control': 'no-store' } })
24
+ }
@@ -0,0 +1,14 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().ACCESS(request, ctx)
10
+ }
11
+
12
+ export function POST(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().ACCESS(request, ctx)
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string; name: string }> }
5
+
6
+ export const maxDuration = 60
7
+
8
+ export function PUT(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().PUT_ASSET(request, ctx)
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function POST(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().FORWARD(request, ctx)
10
+ }
@@ -0,0 +1,18 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().PERSONAL_GET(request, ctx)
10
+ }
11
+
12
+ export function POST(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().PERSONAL_POST(request, ctx)
14
+ }
15
+
16
+ export function DELETE(request: NextRequest, ctx: Ctx) {
17
+ return hubRoutes().PERSONAL_DELETE(request, ctx)
18
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().READS(request, ctx)
10
+ }
@@ -0,0 +1,14 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().RECIPIENTS(request, ctx)
10
+ }
11
+
12
+ export function POST(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().RECIPIENTS(request, ctx)
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function POST(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().REPLY(request, ctx)
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function POST(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().RESOLVE(request, ctx)
10
+ }
@@ -0,0 +1,14 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().RESPONSES(request, ctx)
10
+ }
11
+
12
+ export function DELETE(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().RESPONSES(request, ctx)
14
+ }
@@ -0,0 +1,12 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export function GET(request: NextRequest, ctx: Ctx) {
7
+ return hubRoutes().GET(request, ctx)
8
+ }
9
+
10
+ export function DELETE(request: NextRequest, ctx: Ctx) {
11
+ return hubRoutes().DELETE(request, ctx)
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function POST(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().SEEN(request, ctx)
10
+ }
@@ -0,0 +1,14 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().SETTINGS(request, ctx)
10
+ }
11
+
12
+ export function POST(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().SETTINGS(request, ctx)
14
+ }
@@ -0,0 +1,14 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().STATE_GET(request, ctx)
10
+ }
11
+
12
+ export function POST(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().STATE_POST(request, ctx)
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function POST(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().TRANSCRIBE(request, ctx)
10
+ }
@@ -0,0 +1,11 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string; name: string }> }
5
+
6
+ // Big files (narration) skip the function: this signs a URL the publisher PUTs to directly.
7
+ export const maxDuration = 30
8
+
9
+ export function POST(request: NextRequest, ctx: Ctx) {
10
+ return hubRoutes().UPLOAD(request, ctx)
11
+ }
@@ -0,0 +1,14 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().VERSIONS(request, ctx)
10
+ }
11
+
12
+ export function POST(request: NextRequest, ctx: Ctx) {
13
+ return hubRoutes().VERSIONS(request, ctx)
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ type Ctx = { params: Promise<{ id: string }> }
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export function GET(request: NextRequest, ctx: Ctx) {
9
+ return hubRoutes().VIEWS(request, ctx)
10
+ }
@@ -0,0 +1,13 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export const maxDuration = 30
6
+
7
+ export function POST(request: NextRequest) {
8
+ return hubRoutes().POST(request)
9
+ }
10
+
11
+ export function GET(request: NextRequest) {
12
+ return hubRoutes().LIST(request)
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export function GET(request: NextRequest) {
7
+ return hubRoutes().COMMENTS_FEED(request)
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextRequest } from 'next/server'
2
+ import { hubRoutes } from '@/lib/routes'
3
+ import { healthOf } from '@/lib/health'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ // Whether this hub's queries will be served, the comments feed's Firestore index first. No key.
8
+ export function GET(request: NextRequest) {
9
+ return healthOf(hubRoutes(), request)
10
+ }