@saypsst/next 0.0.0-stage → 0.3.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.
- package/README.md +190 -2
- package/dist/chunk-2NMIB353.js +65 -0
- package/dist/chunk-ELYUDQHH.js +42 -0
- package/dist/chunk-QE3BBW25.js +18 -0
- package/dist/chunk-X3GT7AOS.js +593 -0
- package/dist/chunk-ZELUPPQB.js +95 -0
- package/dist/config-LILlPYgW.d.ts +55 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/llms.d.ts +8 -0
- package/dist/llms.js +16 -0
- package/dist/page.d.ts +41 -0
- package/dist/page.js +190 -0
- package/dist/proxy.d.ts +18 -0
- package/dist/proxy.js +112 -0
- package/dist/sitemap.d.ts +11 -0
- package/dist/sitemap.js +19 -0
- package/dist/style.d.ts +8 -0
- package/dist/style.js +12 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.js +17 -0
- package/package.json +73 -4
package/README.md
CHANGED
|
@@ -1,3 +1,191 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @saypsst/next
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Drop-in route modules that serve [PSST](https://saypsst.com) pages on your own domain, inside your Next.js App Router app (Next 15 or 16, any host). PSST publishes, updates and withdraws pages under a path prefix it owns (default `/resources`) without another deploy on your side.
|
|
4
|
+
|
|
5
|
+
- Server components and route handlers only: no client component, no client JavaScript added.
|
|
6
|
+
- Full text in the HTML for every visitor and crawler, with canonical, Open Graph, JSON-LD and a `psst-version` meta.
|
|
7
|
+
- Time-based ISR (300 s). If PSST is down, your site keeps serving the last good render.
|
|
8
|
+
- No secrets. The only value you configure is your public site id.
|
|
9
|
+
- Runtime dependency: `zod` (pinned). `@psst/content`, the PSST content contract, is bundled into `dist/`. No install scripts.
|
|
10
|
+
|
|
11
|
+
`npx saypsst init` writes all of this for you. This README is the reference for what it writes.
|
|
12
|
+
|
|
13
|
+
## Files you add
|
|
14
|
+
|
|
15
|
+
Paths assume `app/` at the project root and the default prefix `/resources`. With `src/app/`, put `psst.config.ts` in `src/` (the relative imports stay the same). With another prefix, replace `resources` in the folder names and add one `../` per extra segment.
|
|
16
|
+
|
|
17
|
+
> Two Next.js rules shape these files. Route segment config (`revalidate`, `dynamicParams`) must be a literal in the route file, because Next reads it statically: it cannot be re-exported from a package. And a folder whose name starts with `_` is private in the App Router, so the URL segment `/_psst` needs the folder name `%5Fpsst`.
|
|
18
|
+
|
|
19
|
+
### `psst.config.ts`
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { definePsst } from "@saypsst/next";
|
|
23
|
+
|
|
24
|
+
export default definePsst({
|
|
25
|
+
site: "site_xxxxxxxxxxxxxxxx", // public site id from the PSST dashboard
|
|
26
|
+
domain: "example.com", // production host, no scheme
|
|
27
|
+
siteName: "Example",
|
|
28
|
+
prefix: "/resources",
|
|
29
|
+
rootPaths: [], // add "/about" and/or "/llms.txt" only when the dashboard claimed them
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Optional keys: `edge` (defaults to `https://edge.saypsst.com`; `http:` is accepted only on localhost) and `shell: { nav: [{ label, href }], footer }` for the built-in header/footer. `definePsst` validates everything and throws on a bad value, so a typo fails the build instead of serving broken pages.
|
|
34
|
+
|
|
35
|
+
### `app/(psst)/resources/[...slug]/page.tsx`
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { psstPage } from "@saypsst/next/page";
|
|
39
|
+
import psst from "../../../../psst.config";
|
|
40
|
+
|
|
41
|
+
const page = psstPage(psst);
|
|
42
|
+
|
|
43
|
+
export const revalidate = 300;
|
|
44
|
+
export const dynamicParams = true;
|
|
45
|
+
export const generateStaticParams = page.generateStaticParams;
|
|
46
|
+
export const generateMetadata = page.generateMetadata;
|
|
47
|
+
export default page.default;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To get your site's header and footer (needed when your root layout renders none), pass a `Shell`. It receives the PSST `<main>` element as `children`; do not wrap it in another `<main>`:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import type { PsstShellProps } from "@saypsst/next/page";
|
|
54
|
+
import { SiteHeader, SiteFooter } from "@/components/site";
|
|
55
|
+
|
|
56
|
+
function Shell({ children }: PsstShellProps) {
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<SiteHeader />
|
|
60
|
+
{children}
|
|
61
|
+
<SiteFooter />
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const page = psstPage(psst, { Shell });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `app/(psst)/resources/sitemap.xml/route.ts`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { psstSitemap } from "@saypsst/next/sitemap";
|
|
73
|
+
import psst from "../../../../psst.config";
|
|
74
|
+
|
|
75
|
+
export const revalidate = 300;
|
|
76
|
+
export const GET = psstSitemap(psst);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `app/(psst)/resources/%5Fpsst/style.css/route.ts`
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { psstStyle } from "@saypsst/next/style";
|
|
83
|
+
import psst from "../../../../../psst.config";
|
|
84
|
+
|
|
85
|
+
export const revalidate = 300;
|
|
86
|
+
export const GET = psstStyle(psst);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `app/(psst)/resources/%5Fpsst/verify/route.ts`
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { psstVerify } from "@saypsst/next/verify";
|
|
93
|
+
import psst from "../../../../../psst.config";
|
|
94
|
+
|
|
95
|
+
export const revalidate = 300;
|
|
96
|
+
export const GET = psstVerify(psst);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `proxy.ts` (Next 16; `middleware.ts` on Next 15)
|
|
100
|
+
|
|
101
|
+
New file:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { withPsst } from "@saypsst/next/proxy";
|
|
105
|
+
import psst from "./psst.config";
|
|
106
|
+
|
|
107
|
+
export const proxy = withPsst(psst);
|
|
108
|
+
|
|
109
|
+
export const config = {
|
|
110
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
If you already have a proxy, wrap it and keep your own matcher (make sure it also matches the PSST prefix):
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
export const proxy = withPsst(psst, yourProxy);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then reference the PSST sitemap from `robots.txt` (`Sitemap: https://example.com/resources/sitemap.xml`) or from `app/robots.ts`.
|
|
121
|
+
|
|
122
|
+
### Only when the dashboard claimed a root path
|
|
123
|
+
|
|
124
|
+
`app/about/page.tsx` (add `"/about"` to `rootPaths`):
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { psstPage } from "@saypsst/next/page";
|
|
128
|
+
import psst from "../../psst.config";
|
|
129
|
+
|
|
130
|
+
const page = psstPage(psst, { path: "/about" });
|
|
131
|
+
|
|
132
|
+
export const revalidate = 300;
|
|
133
|
+
export const generateMetadata = page.generateMetadata;
|
|
134
|
+
export default page.default;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`app/llms.txt/route.ts` (add `"/llms.txt"` to `rootPaths`):
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { psstLlms } from "@saypsst/next/llms";
|
|
141
|
+
import psst from "../../psst.config";
|
|
142
|
+
|
|
143
|
+
export const revalidate = 300;
|
|
144
|
+
export const GET = psstLlms(psst);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Do not edit these files by hand beyond the `Shell`; PSST content is never added locally.
|
|
148
|
+
|
|
149
|
+
## Exports
|
|
150
|
+
|
|
151
|
+
| Import | Export | What it is |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `@saypsst/next` | `definePsst(input)` | Validates and freezes the config. Throws `PsstConfigError`. |
|
|
154
|
+
| | `PsstConfig`, `PsstConfigInput`, `PsstConfigError`, `PSST_REVALIDATE` (300), `DEFAULT_EDGE`, `PSST_NEXT_VERSION` | Types and constants. |
|
|
155
|
+
| `@saypsst/next/page` | `psstPage(config, { Shell?, path? })` | Returns `{ default, generateMetadata, generateStaticParams }` for the page file. |
|
|
156
|
+
| | `PsstShellProps`, `PsstPageOptions`, `PsstPageProps`, `PsstPageModule`, `PsstIntegrityError`, `PSST_REVALIDATE` | |
|
|
157
|
+
| `@saypsst/next/sitemap` | `psstSitemap(config)` | `GET` handler: XML sitemap of the published PSST pages. |
|
|
158
|
+
| `@saypsst/next/style` | `psstStyle(config)` | `GET` handler: the PSST stylesheet, same-origin, `text/css`. |
|
|
159
|
+
| `@saypsst/next/verify` | `psstVerify(config)` | `GET` handler: the domain-proof text PSST checks. |
|
|
160
|
+
| `@saypsst/next/llms` | `psstLlms(config)` | `GET` handler: `/llms.txt`. Throws unless `/llms.txt` is in `rootPaths`. |
|
|
161
|
+
| `@saypsst/next/proxy` | `withPsst(config, proxy?)`, `PsstProxy` | Wraps your proxy: AI-crawler / AI-referral hits, 410 for withdrawn paths. |
|
|
162
|
+
|
|
163
|
+
## What each module does
|
|
164
|
+
|
|
165
|
+
**Page.** `generateMetadata` and the page fetch `GET {edge}/v1/sites/{site}/pages?path=…` (one request per render; 8 s timeout), validate the response and the document against the content contract (images only from your domain or `edge.saypsst.com`), and check the document's sha256 against the version hash PSST approved. The body has the same structure as PSST's static renderer: header, `main > article.psst-article` (h1, lede, updated date, blocks, sources), footer, plus one JSON-LD script (escaped; the only script). Metadata replaces your layout's defaults: absolute title, description, canonical `https://{domain}{path}`, Open Graph, Twitter, `robots: index, follow`, and `<meta name="psst-version" content="{12-char hash}">`. `generateStaticParams` prerenders the published pages from the manifest at build, and returns `[]` if PSST is unreachable.
|
|
166
|
+
|
|
167
|
+
**Sitemap** renders the manifest's published HTML pages (withdrawn paths are already out). **Style, verify, llms** copy the files PSST's edge renders (`GET {edge}/s/{site}{path}`), check the media type and size, and serve them same-origin with `nosniff`.
|
|
168
|
+
|
|
169
|
+
**Proxy.** On every matched request it classifies the `User-Agent` (a claim, not proof) and the `Referer` host. Known AI crawlers and visitors arriving from ChatGPT, Perplexity, Gemini, Copilot or Claude produce one `POST {edge}/v1/hits` with the path and the classes only: no IP, no cookie, no query string, no UA string. The post is not awaited (`event.waitUntil`, 300 ms abort) and errors are swallowed. Requests with `X-Psst-Doctor: 1`, prefetches, RSC navigations, `/_next/*` and static assets are skipped. Then your proxy runs unchanged and its result is returned as is.
|
|
170
|
+
|
|
171
|
+
## Caching and failure behaviour (Next 16 mechanisms)
|
|
172
|
+
|
|
173
|
+
Verified against the Next 16.4 docs (`incremental-static-regeneration`, `caching-without-cache-components`, `route-segment-config`, `route`, `unstable_rethrow`, `proxy`) and source (`build/templates/app-page-runtime.js`, `app-route.js`, `server/lib/patch-fetch.js`).
|
|
174
|
+
|
|
175
|
+
| Requirement | Mechanism used |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| Time-based ISR, 300 s | Route segment config `export const revalidate = 300` written as a literal in every route file (page and the four route handlers), plus `dynamicParams = true` on the catch-all. Edge fetches set no `cache` option, so they never enter the Data Cache (only status-200 responses would) and the ISR entry is the one cache layer; using no Request-time API keeps the routes static. Pages missing from `generateStaticParams` render on first request and are then cached the same way. |
|
|
178
|
+
| Keep the last good render on fetch failure | The page and handlers **throw** (`PsstUnavailableError`, `PsstIntegrityError`) on a network error, timeout, non-200/404/410 answer, invalid JSON, contract failure or hash mismatch. Next's response cache keeps serving the previous entry when a background regeneration throws, and retries on the next request. Nothing is caught that Next needs: `notFound()` is called outside any `try`, so `unstable_rethrow` is not needed. |
|
|
179
|
+
| Never-rendered page while PSST is down | **A page cannot set a 503 in the App Router** (only `notFound()` 404, redirects, and the experimental 401/403). The closest correct mechanism: the thrown error makes Next answer **500 with `Cache-Control: private, no-cache, no-store`** and a `noindex` robots meta, and the failure is never persisted as ISR output, so no cached 404 and no cached error; the next request retries. There is no `Retry-After` on this response. |
|
|
180
|
+
| PSST down during `next build` | Your build never fails because of PSST. `generateStaticParams` returns `[]` (the pages render on first request). A fixed-path page (`/about`) that cannot fetch at build calls `connection()` (`next/server`), so Next renders it at request time for that deployment. Route handlers (`process.env.NEXT_PHASE === "phase-production-build"`) return **503, `Retry-After: 300`, `Cache-Control: no-store`**; Next never stores a build-time route response with status ≥ 400 other than 404 (it sets `revalidate: 0`), so those routes are also rendered at request time for that deployment. In that degraded mode every request asks the edge: it answers normally once PSST is back (successful handler responses carry `Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400`, so a CDN still caches them), and answers 500 uncached while PSST is down. The next deploy restores full ISR. At runtime the handlers never return an error response: they throw, as above, so a good response is never replaced. |
|
|
181
|
+
| Not found / withdrawn | Edge 404 or 410 → `notFound()` (404, cached for one ISR period). A page cannot answer 410, so `withPsst()` answers **410** for paths in the manifest's `withdrawn` list. It keeps that list in memory per instance and refreshes it in the background every 300 s, so no request waits on PSST; until the first refresh completes, a withdrawn path answers 404. The sitemap drops withdrawn paths. |
|
|
182
|
+
|
|
183
|
+
Cache Components (`cacheComponents: true`) removes the `revalidate` and `dynamicParams` segment configs, so these route files do not build there. Such an app sets `serveFromEdge: true` in `psst.config.ts` and adds only `proxy.ts`: `withPsst()` rewrites PSST paths to the edge, which renders them with its own header and footer (not your layout), answers 410 for withdrawn paths and counts crawler hits itself. `npx saypsst init` does this when it detects the flag.
|
|
184
|
+
|
|
185
|
+
Self-hosted with several instances (for example behind CloudFront): the default ISR cache is per instance. PSST content then refreshes per instance within 300 s, which is correct but uneven; a shared cache handler fixes that.
|
|
186
|
+
|
|
187
|
+
## Security notes
|
|
188
|
+
|
|
189
|
+
- Every text field is escaped by React; links are `https:` or same-site paths; images only from your domain or `edge.saypsst.com`; the document is re-validated before rendering, and its hash must match the approved version.
|
|
190
|
+
- JSON-LD is serialized with `<`, `>`, `&`, U+2028 and U+2029 escaped. It is the only script, and it is a data block, so it fits strict CSPs.
|
|
191
|
+
- Edge requests use `redirect: "error"`, size caps (2 MB JSON, 256 KB CSS, 1 KB proof, 512 KB llms.txt), and a `psst-next/<version>` user agent.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { domainSchema, publicIdSchema, isSafeHref, prefixSchema } from './chunk-X3GT7AOS.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
var PSST_REVALIDATE = 300;
|
|
5
|
+
var DEFAULT_EDGE = "https://edge.saypsst.com";
|
|
6
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
7
|
+
function isEdgeOrigin(value) {
|
|
8
|
+
let url;
|
|
9
|
+
try {
|
|
10
|
+
url = new URL(value);
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return false;
|
|
15
|
+
if (url.pathname !== "/" && url.pathname !== "") return false;
|
|
16
|
+
if (url.protocol === "https:") return true;
|
|
17
|
+
return url.protocol === "http:" && LOCAL_HOSTS.has(url.hostname);
|
|
18
|
+
}
|
|
19
|
+
var configSchema = z.strictObject({
|
|
20
|
+
/** Public site id from the psst dashboard (`site_` + 16 characters). Not a secret. */
|
|
21
|
+
site: publicIdSchema,
|
|
22
|
+
/** Bare production host, e.g. `example.com`. Used for canonical URLs and the image allowlist. */
|
|
23
|
+
domain: domainSchema,
|
|
24
|
+
/** Brand name shown in JSON-LD and Open Graph. */
|
|
25
|
+
siteName: z.string().min(1).max(120),
|
|
26
|
+
/** Path prefix psst owns on the site. */
|
|
27
|
+
prefix: prefixSchema.default("/resources"),
|
|
28
|
+
/** Root paths psst serves outside the prefix (only what the dashboard claimed). */
|
|
29
|
+
rootPaths: z.array(z.enum(["/about", "/llms.txt"])).max(2).default([]),
|
|
30
|
+
/** psst edge origin. Override only for local development. */
|
|
31
|
+
edge: z.string().refine(isEdgeOrigin, "edge must be an https origin (http only on localhost)").default(DEFAULT_EDGE),
|
|
32
|
+
/**
|
|
33
|
+
* Serve psst paths from the edge through `withPsst()` instead of route files, for apps with Cache Components, where
|
|
34
|
+
* the route files do not build. Pages use the edge's own header and footer, not the app's layout.
|
|
35
|
+
*/
|
|
36
|
+
serveFromEdge: z.boolean().default(false),
|
|
37
|
+
/** Header/footer used when the page module gets no `Shell` component. Same shape as the static renderer's shell. */
|
|
38
|
+
shell: z.strictObject({
|
|
39
|
+
nav: z.array(
|
|
40
|
+
z.strictObject({
|
|
41
|
+
label: z.string().min(1).max(60),
|
|
42
|
+
href: z.string().refine(isSafeHref, "nav href must be https: or a same-site path")
|
|
43
|
+
})
|
|
44
|
+
).max(12).optional(),
|
|
45
|
+
footer: z.string().min(1).max(300).optional()
|
|
46
|
+
}).optional()
|
|
47
|
+
});
|
|
48
|
+
var PsstConfigError = class extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "PsstConfigError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function definePsst(input) {
|
|
55
|
+
const parsed = configSchema.safeParse(input);
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
throw new PsstConfigError(
|
|
58
|
+
`invalid PSST config: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const c = parsed.data;
|
|
62
|
+
return Object.freeze({ ...c, edge: c.edge.replace(/\/+$/, "") });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { DEFAULT_EDGE, PSST_REVALIDATE, PsstConfigError, definePsst };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PsstUnavailableError } from './chunk-ZELUPPQB.js';
|
|
2
|
+
|
|
3
|
+
// src/route.ts
|
|
4
|
+
var RETRY_AFTER_SECONDS = 300;
|
|
5
|
+
function isBuildPhase() {
|
|
6
|
+
return process.env["NEXT_PHASE"] === "phase-production-build";
|
|
7
|
+
}
|
|
8
|
+
function psstRoute(handler) {
|
|
9
|
+
return async () => {
|
|
10
|
+
try {
|
|
11
|
+
return await handler();
|
|
12
|
+
} catch (err) {
|
|
13
|
+
if (err instanceof PsstUnavailableError && isBuildPhase()) {
|
|
14
|
+
console.warn(`${err.message}; serving 503 until the next regeneration`);
|
|
15
|
+
return unavailable();
|
|
16
|
+
}
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function unavailable() {
|
|
22
|
+
return new Response("PSST is temporarily unavailable\n", {
|
|
23
|
+
status: 503,
|
|
24
|
+
headers: {
|
|
25
|
+
"content-type": "text/plain; charset=utf-8",
|
|
26
|
+
"retry-after": String(RETRY_AFTER_SECONDS),
|
|
27
|
+
"cache-control": "no-store"
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function notFoundResponse() {
|
|
32
|
+
return new Response("Not found\n", { status: 404, headers: { "content-type": "text/plain; charset=utf-8" } });
|
|
33
|
+
}
|
|
34
|
+
var SUCCESS_CACHE_CONTROL = "public, max-age=0, s-maxage=300, stale-while-revalidate=86400";
|
|
35
|
+
function textResponse(body, contentType) {
|
|
36
|
+
return new Response(body, {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "content-type": contentType, "x-content-type-options": "nosniff", "cache-control": SUCCESS_CACHE_CONTROL }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { isBuildPhase, notFoundResponse, psstRoute, textResponse };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { psstRoute, notFoundResponse, textResponse } from './chunk-ELYUDQHH.js';
|
|
2
|
+
import { fetchEdgeFile, PsstUnavailableError, readCapped } from './chunk-ZELUPPQB.js';
|
|
3
|
+
|
|
4
|
+
// src/edge-file.ts
|
|
5
|
+
function edgeFileRoute(config, spec) {
|
|
6
|
+
return psstRoute(async () => {
|
|
7
|
+
const res = await fetchEdgeFile(config, spec.path, spec.type);
|
|
8
|
+
if (res.status === 404 || res.status === 410) return notFoundResponse();
|
|
9
|
+
if (res.status !== 200) throw new PsstUnavailableError(`edge answered ${res.status} for ${spec.path}`);
|
|
10
|
+
const type = (res.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
11
|
+
if (type !== spec.type) throw new PsstUnavailableError(`edge answered ${type || "no type"} for ${spec.path}`);
|
|
12
|
+
const body = await readCapped(res, spec.maxBytes);
|
|
13
|
+
if (spec.accept && !spec.accept(body)) throw new PsstUnavailableError(`edge body for ${spec.path} failed its check`);
|
|
14
|
+
return textResponse(body, `${spec.type}; charset=utf-8`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { edgeFileRoute };
|