@strifeapp/astro 1.5.0 → 1.5.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.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Official [Strife](https://strife.app) integration for [Astro](https://astro.buil
11
11
  - [Usage](#usage)
12
12
  - [Reading content](#reading-content)
13
13
  - [Telemetry: `<Insights />`](#telemetry-insights)
14
- - [Share links](#share-links)
14
+ - [Snapshots](#snapshots)
15
15
  - [TypeScript: typing `strife:store`](#typescript-typing-strifestore)
16
16
  - [Direct Vite plugin](#direct-vite-plugin)
17
17
  - [Configuration](#configuration)
@@ -103,20 +103,20 @@ On every page load it reports Core Web Vitals (LCP, INP, CLS, FCP, TTFB) and, wh
103
103
 
104
104
  > **Content Security Policy:** if your site sends a CSP header, allow the insights endpoint in `connect-src` (e.g. `connect-src 'self' https://api.strife.app`) — otherwise the browser blocks the beacon (a CSP violation is logged to the console; no request is sent and nothing is stored). Use your custom `endpoint`'s origin if you overrode it.
105
105
 
106
- Inside the Strife live preview (`Astro.locals.editMode`) only the accessibility audit runs; on a [share link](#share-links) (`Astro.locals.share`) the component renders nothing and sends no beacon at all.
106
+ Inside the Strife live preview (`Astro.locals.editMode`) only the accessibility audit runs; on a [snapshot](#snapshots) (`Astro.locals.snapshot`) the component renders nothing and sends no beacon at all.
107
107
 
108
- ### Share links
108
+ ### Snapshots
109
109
 
110
- An editor can hand a reviewer a link to a frozen, unpublished version of a document: `{origin}{locale root prefix}/share/{guid}` — for example `https://example.com/share/0123…cdef` or `https://example.com/sv/share/0123…cdef`. The integration registers a `pre` middleware (`@strifeapp/astro/share-middleware`) that resolves such a request to its share snapshot in the `Content/ByUrl` index — for the locale Astro resolved (`context.currentLocale`) — and either sets `Astro.locals.share` or answers with **your 404 page** (unknown, expired or revoked links look like content that does not exist). Both responses carry `X-Robots-Tag: noindex, nofollow`, `Cache-Control: private, no-store` and `Referrer-Policy: no-referrer`; edit mode is forced off on share routes, so `<LivePreview />` never activates there.
110
+ An editor can hand a reviewer a link to a frozen, unpublished version of a document: `{origin}{locale root prefix}/snapshot/{token}` — for example `https://example.com/snapshot/0123…cdef` or `https://example.com/sv/snapshot/0123…cdef`. The integration registers a `pre` middleware (`@strifeapp/astro/snapshot-middleware`) that resolves such a request to its snapshot in the `Content/ByUrl` index — for the locale Astro resolved (`context.currentLocale`) — and either sets `Astro.locals.snapshot` or answers with **your 404 page** (unknown, expired or revoked links look like content that does not exist). Both responses carry `X-Robots-Tag: noindex, nofollow`, `Cache-Control: private, no-store` and `Referrer-Policy: no-referrer`; edit mode is forced off on snapshot routes, so `<LivePreview />` never activates there.
111
111
 
112
- You author the page — it renders in your own layout. Copy [`templates/share-page.astro`](https://github.com/wieldyapp/wieldy/blob/master/src/sdk/js/src/packages/astro/templates/share-page.astro) to `src/pages/share/[guid].astro`:
112
+ You author the page — it renders in your own layout. Copy [`templates/snapshot-page.astro`](https://github.com/wieldyapp/wieldy/blob/master/src/sdk/js/src/packages/astro/templates/snapshot-page.astro) to `src/pages/snapshot/[token].astro`:
113
113
 
114
114
  ```astro
115
115
  ---
116
116
  export const prerender = false; // rendered on demand — never prebuilt
117
117
  import Layout from '../../layouts/Layout.astro';
118
118
 
119
- const { name, expiresAt, locale, content } = Astro.locals.share!;
119
+ const { name, expiresAt, locale, content } = Astro.locals.snapshot!;
120
120
  ---
121
121
  <Layout title={content.displayName ?? 'Preview'}>
122
122
  <aside role="status">{name ? `Preview “${name}”` : 'Preview'} — expires {expiresAt.toISOString()}</aside>
@@ -124,45 +124,46 @@ const { name, expiresAt, locale, content } = Astro.locals.share!;
124
124
  </Layout>
125
125
  ```
126
126
 
127
- `Astro.locals.share` is `{ docId, name, expiresAt: Date, locale, content }`. `content` is the index row for that locale: `docId`, `locale`, `collection`, `displayName`, `url`, `shareName`, `expiresAt`, plus every field you list in `share.fields`, so one lookup both decides the 404 and feeds the render:
127
+ `Astro.locals.snapshot` is `{ docId, name, expiresAt: Date, locale, content }`. `content` is the index row for that locale: `docId`, `locale`, `collection`, `displayName`, `url` (the snapshot path itself — a snapshot row's `url` is `{root slug}/snapshot/{token}` for its locale, never the canonical page's path), `snapshotName`, `expiresAt`, plus every field you list in `snapshot.fields`, so one lookup both decides the 404 and feeds the render:
128
128
 
129
129
  ```typescript
130
130
  strife({
131
- share: {
132
- fields: ['heading', 'body'], // your template fields, projected onto Astro.locals.share.content
131
+ snapshot: {
132
+ fields: ['heading', 'body'], // your template fields, projected onto Astro.locals.snapshot.content
133
133
  },
134
134
  });
135
135
  ```
136
136
 
137
- Without `share.fields`, `content` carries the row fields only and the page queries its own fields by `docId` + `locale` (`whereEquals('docId', content.docId)`, `whereEquals('status', 'share')`).
137
+ Without `snapshot.fields`, `content` carries the row fields only and the page queries its own fields by `docId` + `locale` (`whereEquals('docId', content.docId)`, `whereEquals('status', 'snapshot')`).
138
138
 
139
139
  | Option | Where | Description |
140
140
  | --- | --- | --- |
141
- | `share.fields` | integration | Extra `Content/ByUrl` fields to project onto `content`. |
142
- | `share.middleware: false` | integration | Skip the automatic registration; compose the middleware yourself (below). |
143
- | `resolveLocale(context)` | `createShareMiddleware` | Resolve the request's locale when your site does not use Astro i18n routing. Defaults to `context.currentLocale`. |
141
+ | `snapshot.fields` | integration | Extra `Content/ByUrl` fields to project onto `content`. |
142
+ | `snapshot.middleware: false` | integration | Skip the automatic registration; compose the middleware yourself (below). |
143
+ | `resolveLocale(context)` | `createSnapshotMiddleware` | Resolve the request's locale when your site does not use Astro i18n routing. Defaults to `context.currentLocale`. |
144
144
 
145
- When the resolved locale has no row in the snapshot (a cookie- or Accept-Language-driven locale the draft never had, a link pasted under another prefix), the middleware serves the snapshot's own `shareLocale` row instead of the 404 — the same fallback as the .NET SDK. If your site resolves its locale in its own middleware (a cookie, a header, its own path scheme), Astro's `currentLocale` is not enough — opt out of the automatic registration and compose the middleware where your locale is known:
145
+ When the resolved locale has no row in the snapshot (a cookie- or Accept-Language-driven locale the draft never had, a link pasted under another prefix), the middleware serves the snapshot's own `snapshotLocale` row instead of the 404 — the same fallback as the .NET SDK. If your site resolves its locale in its own middleware (a cookie, a header, its own path scheme), Astro's `currentLocale` is not enough — opt out of the automatic registration and compose the middleware where your locale is known:
146
146
 
147
147
  ```typescript
148
148
  // src/middleware.ts
149
149
  import { sequence } from 'astro:middleware';
150
- import { createShareMiddleware } from '@strifeapp/astro/share-middleware';
150
+ import { createSnapshotMiddleware } from '@strifeapp/astro/snapshot-middleware';
151
151
 
152
152
  export const onRequest = sequence(
153
153
  myLocaleMiddleware,
154
- createShareMiddleware({ resolveLocale: (context) => context.locals.locale }),
154
+ createSnapshotMiddleware({ resolveLocale: (context) => context.locals.locale }),
155
155
  );
156
156
  ```
157
157
 
158
158
  Keep these in mind:
159
159
 
160
- - The share guid is the link's secret. Render `name` as text only (never `set:html`), emit no Open Graph tags on the page, keep `/share/*` out of CDN caching rules and analytics, and keep access-log retention short — a bearer URL is inherently logged by proxies. The middleware logs nothing on a hit and never logs the guid.
161
- - `/share/{guid}` and `/{one segment}/share/{guid}` are reserved by the convention; the prefix segment is opaque (the locale comes from your resolution, not the path). Only a tail of exactly 32 lowercase hex characters is a share request — a share-shaped path with any other tail (`/help/share/link`) is not claimed by the middleware and reaches your own routes, as in the .NET SDK.
160
+ - The snapshot token is the link's secret. Render `name` as text only (never `set:html`), emit no Open Graph tags on the page, keep `/snapshot/*` out of CDN caching rules and analytics, and keep access-log retention short — a bearer URL is inherently logged by proxies. The middleware logs nothing on a hit and never logs the token.
161
+ - `/snapshot/{token}` and `/{one segment}/snapshot/{token}` are reserved by the convention; the prefix segment is opaque (the locale comes from your resolution, not the path). Only a tail of 8 lowercase letters or digits is a snapshot request — a snapshot-shaped path with any other tail (`/help/snapshot/link`) is not claimed by the middleware and reaches your own routes, as in the .NET SDK.
162
162
  - A miss is rendered through Astro's rewrite to `/404`. Astro forbids rewriting from an on-demand route to a **prerendered** page, so if your `src/pages/404.astro` is static the middleware answers a bare `404` (headers included) and your adapter or host serves the static 404 page; add `export const prerender = false` to `404.astro` to render it through the rewrite instead.
163
- - Share links need a `Content/ByUrl` index built from a `@strifeapp/strife` release that includes share links, deployed with `strife push` (the developer docs state the exact versions); until then Strife refuses to create shares for the team, and the route answers your 404 page.
163
+ - Snapshots need a `Content/ByUrl` index built from a `@strifeapp/strife` release that includes snapshots, deployed with `strife push` (the developer docs state the exact versions); until then Strife refuses to create snapshots for the team, and the route answers your 404 page.
164
+ - The middleware is optional: because a snapshot row's `url` is the snapshot path, a site can serve snapshots through its ordinary exact-URL lookup instead — the raw contract (the one predicate clause, the load-bearing locale clause, per-request resolution, and the hygiene you then own) is documented in the Strife developer reference under Snapshots.
164
165
 
165
- `resolveShare(guid, locale)` from `@strifeapp/astro/share` is the same lookup as a function, returning the share context or `null`.
166
+ `resolveSnapshot(token, locale)` from `@strifeapp/astro/snapshot` is the same lookup as a function, returning the snapshot context or `null`.
166
167
 
167
168
  ### TypeScript: typing `strife:store`
168
169
 
@@ -68,8 +68,8 @@ const site = resolveSiteId();
68
68
  // while the heavy axe-core accessibility audit runs only in the editor's live
69
69
  // preview (`editMode`) — a11y is deterministic per content version, so the editor is
70
70
  // the right place for it, and it keeps axe-core off every real visitor entirely. A
71
- // share link (`share`, STR-2165) is neither: it sends nothing at all — it is not
72
- // audience traffic, and a beacon would carry the share path, i.e. the guid. Without
71
+ // snapshot (`snapshot`, STR-2165) is neither: it sends nothing at all — it is not
72
+ // audience traffic, and a beacon would carry the snapshot path, i.e. the token. Without
73
73
  // the middleware, plain sites get Web Vitals exactly as before.
74
74
  const mode = insightsMode(Astro.locals);
75
75
  ---
@@ -245,7 +245,7 @@ const mode = insightsMode(Astro.locals);
245
245
  // A real visitor: Web Vitals only, batched on pagehide. No axe-core download/run.
246
246
  initWebVitals();
247
247
  }
248
- // 'off' (a share link) never reaches this script: the component renders nothing.
248
+ // 'off' (a snapshot) never reaches this script: the component renders nothing.
249
249
  </script>
250
250
  </Fragment>
251
251
  )}
package/dist/index.d.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  import { AstroIntegration } from 'astro';
2
2
  import { IAuthOptions } from 'ravendb';
3
- import { ShareContext } from './share';
4
- export type { ShareContent, ShareContext } from './share';
3
+ import { SnapshotContext } from './snapshot';
4
+ export type { SnapshotContent, SnapshotContext } from './snapshot';
5
5
  export interface IntegrationOptions extends IAuthOptions {
6
6
  certificate?: string;
7
7
  password?: string;
8
8
  urls?: string[];
9
9
  database?: string;
10
10
  collections?: Collection[];
11
- /** Share links (`/share/{guid}`); see `@strifeapp/astro/share-middleware`. */
12
- share?: ShareOptions;
11
+ /** Snapshots (`/snapshot/{token}`); see `@strifeapp/astro/snapshot-middleware`. */
12
+ snapshot?: SnapshotOptions;
13
13
  }
14
- export interface ShareOptions {
14
+ export interface SnapshotOptions {
15
15
  /**
16
- * Index fields to project onto `Astro.locals.share.content` beside the row fields
16
+ * Index fields to project onto `Astro.locals.snapshot.content` beside the row fields
17
17
  * (your template fields, e.g. `['heading', 'body']`), so the middleware's one
18
18
  * lookup also feeds the render. Without it, `content` carries the row fields only
19
- * and the share page queries its own fields by `docId` + `locale`.
19
+ * and the snapshot page queries its own fields by `docId` + `locale`.
20
20
  */
21
21
  fields?: string[];
22
22
  /**
23
- * `false` skips the automatic `pre` registration of the share middleware. Do this
23
+ * `false` skips the automatic `pre` registration of the snapshot middleware. Do this
24
24
  * when your site resolves its locale in its own middleware: compose
25
- * `createShareMiddleware({ resolveLocale })` from `@strifeapp/astro/share-middleware`
25
+ * `createSnapshotMiddleware({ resolveLocale })` from `@strifeapp/astro/snapshot-middleware`
26
26
  * in `src/middleware.ts` instead.
27
27
  */
28
28
  middleware?: boolean;
@@ -42,13 +42,13 @@ declare global {
42
42
  */
43
43
  editMode?: boolean;
44
44
  /**
45
- * Set by the share middleware on a `/share/{guid}` request that resolved to a
46
- * live share snapshot: the snapshot's id, its editor-given name (text only —
45
+ * Set by the snapshot middleware on a `/snapshot/{token}` request that resolved to a
46
+ * live snapshot: the snapshot's id, its editor-given name (text only —
47
47
  * never `set:html`), when the link expires, the resolved locale, and the
48
48
  * `Content/ByUrl` projection to render. Absent on every other request, and
49
- * on every share miss (the middleware answers those with the site's 404 page).
49
+ * on every snapshot miss (the middleware answers those with the site's 404 page).
50
50
  */
51
- share?: ShareContext;
51
+ snapshot?: SnapshotContext;
52
52
  }
53
53
  }
54
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE1D,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,8EAA8E;IAC9E,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,GAAG,CAAC;QACZ,UAAU,MAAM;YACd;;;;;eAKG;YACH,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB;;;;;;eAMG;YACH,KAAK,CAAC,EAAE,YAAY,CAAC;SACtB;KACF;CACF;AAMD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,gBAAgB,CAoExF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,GAAG,CAAC;QACZ,UAAU,MAAM;YACd;;;;;eAKG;YACH,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB;;;;;;eAMG;YACH,QAAQ,CAAC,EAAE,eAAe,CAAC;SAC5B;KACF;CACF;AAMD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,gBAAgB,CAoExF"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { envField as e } from "astro/config";
2
- import { v as i } from "./vite-plugin-strife-store-Ch0cgrwA.js";
2
+ import { v as i } from "./vite-plugin-strife-store-B5tSnH_D.js";
3
3
  const c = {
4
4
  type: "pfx"
5
5
  };
@@ -37,8 +37,8 @@ function l(t) {
37
37
  }), r({
38
38
  entrypoint: "@strifeapp/astro/edit-mode-middleware",
39
39
  order: "pre"
40
- }), s.share?.middleware !== !1 && r({
41
- entrypoint: "@strifeapp/astro/share-middleware",
40
+ }), s.snapshot?.middleware !== !1 && r({
41
+ entrypoint: "@strifeapp/astro/snapshot-middleware",
42
42
  order: "pre"
43
43
  });
44
44
  }
@@ -2,13 +2,13 @@
2
2
  * Which telemetry `<Insights />` runs for a request, from the middleware-set locals:
3
3
  * - `vitals`: a real visitor — Core Web Vitals only;
4
4
  * - `audit`: the Strife live preview (edit mode) — the axe-core accessibility audit only;
5
- * - `off`: a share link (R14) — nothing at all. A share is not audience traffic, and a
6
- * beacon would carry the share path, i.e. the guid.
5
+ * - `off`: a snapshot (R14) — nothing at all. A snapshot is not audience traffic, and a
6
+ * beacon would carry the snapshot path, i.e. the token.
7
7
  * Pure, so the gate is unit-tested; Insights.astro only forwards the result.
8
8
  */
9
9
  export type InsightsMode = 'vitals' | 'audit' | 'off';
10
10
  export declare function insightsMode(locals: {
11
11
  editMode?: boolean;
12
- share?: unknown;
12
+ snapshot?: unknown;
13
13
  }): InsightsMode;
14
14
  //# sourceMappingURL=insights-mode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"insights-mode.d.ts","sourceRoot":"","sources":["../src/insights-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;AAEtD,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,YAAY,CAI1F"}
1
+ {"version":3,"file":"insights-mode.d.ts","sourceRoot":"","sources":["../src/insights-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;AAEtD,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,YAAY,CAI7F"}
@@ -1,5 +1,5 @@
1
1
  function e(t) {
2
- return t.share != null ? "off" : t.editMode === !0 ? "audit" : "vitals";
2
+ return t.snapshot != null ? "off" : t.editMode === !0 ? "audit" : "vitals";
3
3
  }
4
4
  export {
5
5
  e as insightsMode
@@ -0,0 +1,36 @@
1
+ import { APIContext, MiddlewareHandler } from 'astro';
2
+ import { ResolveSnapshotOptions } from './snapshot';
3
+ /**
4
+ * Snapshot-link middleware (STR-2165). Registered `pre` by the integration (see
5
+ * src/index.ts) unless `snapshot.middleware` is `false`; also exported as
6
+ * `createSnapshotMiddleware` so a site that resolves its locale in its own middleware
7
+ * can compose it there with `resolveLocale`.
8
+ *
9
+ * For a snapshot path (`/snapshot/{token}` after an optional locale root prefix, where the
10
+ * tail is a valid token — 8 lowercase letters or digits) it looks the snapshot up for
11
+ * the site's resolved locale and:
12
+ * - on a hit sets `Astro.locals.snapshot = { docId, name, expiresAt, locale, content }`
13
+ * and lets the site's `src/pages/snapshot/[token].astro` render it;
14
+ * - on any miss renders the site's 404 page through Astro's rewrite, re-wrapped so
15
+ * the status is 404 (Astro answers a rewrite with 200).
16
+ * Both carry `X-Robots-Tag: noindex, nofollow`, `Cache-Control: private, no-store`
17
+ * and `Referrer-Policy: no-referrer`. Edit mode is forced off on snapshot routes, so
18
+ * `<LivePreview />` never activates there. Nothing is logged on a hit, and the token
19
+ * is never logged. Every other path passes through untouched — including a
20
+ * snapshot-shaped path whose tail is not a token (`/help/snapshot/link`): as in the .NET
21
+ * resolver, that is not a snapshot request but an ordinary site route.
22
+ */
23
+ export interface SnapshotMiddlewareOptions extends ResolveSnapshotOptions {
24
+ /**
25
+ * Resolve the locale of the current request. Defaults to Astro's
26
+ * `context.currentLocale` (i18n routing). A site that resolves its locale itself
27
+ * — from a cookie, a header, its own path scheme — must pass this, or a
28
+ * multi-locale snapshot resolves the wrong (or no) row.
29
+ */
30
+ resolveLocale?: (context: APIContext) => string | undefined | Promise<string | undefined>;
31
+ }
32
+ export declare const SNAPSHOT_RESPONSE_HEADERS: Readonly<Record<string, string>>;
33
+ export declare function createSnapshotMiddleware(options?: SnapshotMiddlewareOptions): MiddlewareHandler;
34
+ /** The default instance the integration registers via `addMiddleware`. */
35
+ export declare const onRequest: MiddlewareHandler;
36
+ //# sourceMappingURL=snapshot-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-middleware.d.ts","sourceRoot":"","sources":["../src/snapshot-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAsD,KAAK,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE7G;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,yBAA0B,SAAQ,sBAAsB;IACvE;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3F;AAED,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIrE,CAAC;AAEH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,yBAA8B,GAAG,iBAAiB,CA0BnG;AAED,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,iBAA8C,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { matchSnapshotPath as u, isSnapshotToken as i, lookupSnapshot as h } from "./snapshot.js";
2
+ const f = Object.freeze({
3
+ "X-Robots-Tag": "noindex, nofollow",
4
+ "Cache-Control": "private, no-store",
5
+ "Referrer-Policy": "no-referrer"
6
+ });
7
+ function p(r = {}) {
8
+ return async (e, o) => {
9
+ const t = u(e.url.pathname);
10
+ if (t === null || !i(t)) return o();
11
+ e.locals.editMode = !1;
12
+ const a = r.resolveLocale ? await r.resolveLocale(e) : e.currentLocale, n = await h(t, a, r);
13
+ return n.snapshot === null ? (n.reason === "query_error" && console.warn("[strife] snapshot lookup failed (reason=query_error); answering 404"), d(o)) : (e.locals.snapshot = n.snapshot, l(await o()));
14
+ };
15
+ }
16
+ const y = p();
17
+ async function d(r) {
18
+ let e;
19
+ try {
20
+ e = await r("/404");
21
+ } catch {
22
+ e = new Response(null, { status: 404 });
23
+ }
24
+ return l(e, 404);
25
+ }
26
+ function l(r, e) {
27
+ const o = e !== void 0 && r.status !== e ? s(r, e) : r, t = (a) => {
28
+ for (const [n, c] of Object.entries(f))
29
+ a.set(n, c);
30
+ };
31
+ try {
32
+ return t(o.headers), o;
33
+ } catch {
34
+ const a = s(o, o.status);
35
+ return t(a.headers), a;
36
+ }
37
+ }
38
+ function s(r, e) {
39
+ return new Response(r.body, {
40
+ status: e,
41
+ statusText: r.statusText,
42
+ headers: new Headers(r.headers)
43
+ });
44
+ }
45
+ export {
46
+ f as SNAPSHOT_RESPONSE_HEADERS,
47
+ p as createSnapshotMiddleware,
48
+ y as onRequest
49
+ };
@@ -0,0 +1,129 @@
1
+ import { IDocumentStore } from 'ravendb';
2
+ /**
3
+ * Snapshots (STR-2165): resolve a `/snapshot/{token}` request to the one live snapshot
4
+ * snapshot its token names, reading the `Content/ByUrl` projection for the resolved
5
+ * locale. Locale handling mirrors the .NET SDK's `DefaultRouteResolver` exactly:
6
+ * - with a resolved locale the query carries a locale clause; when it yields no row
7
+ * (a cookie- or Accept-Language-driven locale the draft never had, a link pasted
8
+ * under another prefix) the token is re-queried without the clause (bounded to
9
+ * `MAX_SNAPSHOT_LOCALE_ROWS`) and only the snapshot's own row — `locale` equal to
10
+ * its `snapshotLocale` — is kept, so the preview is served rather than hidden;
11
+ * - without a resolved locale the token-only query pages every row of the snapshot
12
+ * (again bounded to `MAX_SNAPSHOT_LOCALE_ROWS`, never just the first two), and more
13
+ * than one candidate row is decided by the same rule: the `snapshotLocale` row wins,
14
+ * and anything other than exactly one such row is an `ambiguous_locale` miss.
15
+ * Pure with respect to Astro — the store is injectable, so the lookup is
16
+ * unit-testable without RavenDB; `resolveSnapshot` without a store reads the
17
+ * integration's `strife:store` virtual module.
18
+ *
19
+ * The token is the bearer secret of the link: nothing in this module logs it, and
20
+ * a thrown query (an index build that predates snapshots, an unreachable store)
21
+ * is a miss, never an exception that could surface it on an error page.
22
+ */
23
+ /** The content index the integration queries; deployed by `strife push`. */
24
+ export declare const SNAPSHOT_INDEX_NAME = "Content/ByUrl";
25
+ /** A snapshot token is exactly 8 lowercase letters or digits — nothing looser. */
26
+ export declare const SNAPSHOT_TOKEN_PATTERN: RegExp;
27
+ /**
28
+ * Upper bound on locales one snapshot can carry rows for; a token-only query (no
29
+ * resolved locale, or the locale fallback) never scans further. Same constant as the
30
+ * .NET resolver's `MaxSnapshotLocaleRows`.
31
+ */
32
+ export declare const MAX_SNAPSHOT_LOCALE_ROWS = 64;
33
+ /**
34
+ * Row fields every snapshot lookup projects from the index: the snapshot metadata plus
35
+ * what a page needs to pick a layout (`collection`, `displayName`, `url`). The row's
36
+ * `url` is the snapshot path itself — `{root slug}/snapshot/{token}` for the row's locale,
37
+ * the exact-URL lookup key a raw site queries — never the canonical page's path. `deleted`
38
+ * is not stored in the index; RavenDB's default projection behaviour reads it from
39
+ * the document, which is exactly the soft-delete check we want. Site-specific
40
+ * template fields come from the `fields` option.
41
+ */
42
+ export declare const SNAPSHOT_ROW_FIELDS: readonly ["docId", "locale", "collection", "displayName", "url", "status", "snapshotToken", "snapshotName", "expiresAt", "snapshotLocale", "createdAt", "changedAt", "publishedAt", "origin", "labels", "deleted"];
43
+ /**
44
+ * The `Content/ByUrl` row for the snapshot in the resolved locale — the same
45
+ * projected, locale-translated object a page gets when it queries the index, plus
46
+ * any site fields passed through `fields`.
47
+ */
48
+ export interface SnapshotContent {
49
+ docId: string;
50
+ locale: string;
51
+ status: 'snapshot';
52
+ snapshotToken: string;
53
+ snapshotName?: string | null;
54
+ expiresAt: string;
55
+ /** The locale the editor was viewing when sharing — the one the link targets. */
56
+ snapshotLocale?: string | null;
57
+ collection?: string | null;
58
+ displayName?: string | null;
59
+ /** The snapshot path for this locale (`{root slug}/snapshot/{token}`) — not the canonical page's path. */
60
+ url?: string | null;
61
+ [field: string]: unknown;
62
+ }
63
+ /** What a snapshot page sees on `Astro.locals.snapshot`. */
64
+ export interface SnapshotContext {
65
+ /** The snapshot's document id (`{canonicalId}/snapshot/{token}`). Never log it. */
66
+ docId: string;
67
+ /** The editor-given name, or `null`. Render as text only — never `set:html`. */
68
+ name: string | null;
69
+ /** UTC instant after which the link stops resolving. */
70
+ expiresAt: Date;
71
+ /** The locale the row was resolved for. */
72
+ locale: string;
73
+ /** The index projection for that locale; render this. */
74
+ content: SnapshotContent;
75
+ }
76
+ export type SnapshotMissReason = 'malformed_token' | 'not_found' | 'ambiguous_locale' | 'deleted' | 'no_expiry' | 'expired' | 'query_error';
77
+ export type SnapshotLookup = {
78
+ snapshot: SnapshotContext;
79
+ reason: null;
80
+ } | {
81
+ snapshot: null;
82
+ reason: SnapshotMissReason;
83
+ };
84
+ export interface ResolveSnapshotOptions {
85
+ /** The document store to query. Defaults to the integration's `strife:store`. */
86
+ store?: IDocumentStore;
87
+ /**
88
+ * Extra index fields to project onto `content` (your template fields, e.g.
89
+ * `['heading', 'body']`), so one lookup both decides the 404 and feeds the render.
90
+ * Defaults to the integration's `snapshot.fields` option.
91
+ */
92
+ fields?: readonly string[];
93
+ /** Clock, for tests. */
94
+ now?: () => Date;
95
+ }
96
+ export declare function isSnapshotToken(value: unknown): value is string;
97
+ /**
98
+ * Matches the snapshot convention `/snapshot/{token}` after an optional single locale root
99
+ * prefix segment (`/sv/snapshot/{token}`, `/svenska/snapshot/{token}`), with or without a
100
+ * trailing slash. Returns the raw token segment — validate it with `isSnapshotToken` — or
101
+ * `null` when the path is not snapshot-shaped. The prefix is opaque: the locale comes
102
+ * from the site's own resolution, never from the path.
103
+ */
104
+ export declare function matchSnapshotPath(pathname: string): string | null;
105
+ /**
106
+ * Picks the single live snapshot row or names the miss. Mirrors the .NET SDK's
107
+ * `DefaultRouteResolver.ClassifySnapshotRows` so both site SDKs agree on "live": with
108
+ * more than one row (several locales of one snapshot and no locale resolver to pick
109
+ * between them) the snapshot's own locale wins — the row whose `locale` equals its
110
+ * `snapshotLocale` — and anything other than exactly one such row is `ambiguous_locale`.
111
+ */
112
+ export declare function classifySnapshotRows(rows: readonly Record<string, unknown>[], now: Date): SnapshotLookup;
113
+ /**
114
+ * Looks a snapshot up and reports the outcome with its miss reason. A malformed token
115
+ * never reaches the store. `locale` is the site's resolved locale: the query carries
116
+ * it as a clause, and when that yields no row the token is re-queried without the
117
+ * clause and only the snapshot's own `snapshotLocale` row is kept (the .NET resolver's
118
+ * locale fallback). When `locale` is `undefined` (a single-locale site without Astro
119
+ * i18n) the clause is omitted from the start and the query pages every row of the
120
+ * snapshot; more than one row then resolves to the `snapshotLocale` row, or is an
121
+ * `ambiguous_locale` miss (see `classifySnapshotRows`).
122
+ */
123
+ export declare function lookupSnapshot(token: string, locale: string | undefined, options?: ResolveSnapshotOptions): Promise<SnapshotLookup>;
124
+ /**
125
+ * Resolves `token` in `locale` to the snapshot context, or `null` on any miss
126
+ * (unknown, malformed, deleted, expired, missing expiry, query failure).
127
+ */
128
+ export declare function resolveSnapshot(token: string, locale: string | undefined, options?: ResolveSnapshotOptions): Promise<SnapshotContext | null>;
129
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AAEnD,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,oNAiBtB,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,0GAA0G;IAC1G,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,wDAAwD;IACxD,SAAS,EAAE,IAAI,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAC1B,iBAAiB,GACjB,WAAW,GACX,kBAAkB,GAClB,SAAS,GACT,WAAW,GACX,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GAC3C;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,sBAAsB;IACrC,iFAAiF;IACjF,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGjE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,cAAc,CA2BxG;AAcD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,cAAc,CAAC,CAezB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAEjC"}
@@ -0,0 +1,104 @@
1
+ const i = "Content/ByUrl", c = /^[a-z0-9]{8}$/, _ = 64, p = [
2
+ "docId",
3
+ "locale",
4
+ "collection",
5
+ "displayName",
6
+ "url",
7
+ "status",
8
+ "snapshotToken",
9
+ "snapshotName",
10
+ "expiresAt",
11
+ "snapshotLocale",
12
+ "createdAt",
13
+ "changedAt",
14
+ "publishedAt",
15
+ "origin",
16
+ "labels",
17
+ "deleted"
18
+ ];
19
+ function h(e) {
20
+ return typeof e == "string" && c.test(e);
21
+ }
22
+ function w(e) {
23
+ const n = /^\/(?:[^/]+\/)?snapshot\/([^/]+)\/?$/.exec(e);
24
+ return n ? n[1] : null;
25
+ }
26
+ function f(e, n) {
27
+ if (e.length === 0) return { snapshot: null, reason: "not_found" };
28
+ let t = e[0];
29
+ if (e.length > 1) {
30
+ const r = e.filter(u);
31
+ if (r.length !== 1) return { snapshot: null, reason: "ambiguous_locale" };
32
+ t = r[0];
33
+ }
34
+ if (t.deleted === !0) return { snapshot: null, reason: "deleted" };
35
+ const o = d(t.expiresAt);
36
+ if (o === null) return { snapshot: null, reason: "no_expiry" };
37
+ if (o.getTime() <= n.getTime()) return { snapshot: null, reason: "expired" };
38
+ const s = t;
39
+ return {
40
+ snapshot: {
41
+ docId: String(s.docId),
42
+ name: typeof s.snapshotName == "string" ? s.snapshotName : null,
43
+ expiresAt: o,
44
+ locale: String(s.locale),
45
+ content: s
46
+ },
47
+ reason: null
48
+ };
49
+ }
50
+ function u(e) {
51
+ return e.snapshotLocale != null && e.locale === e.snapshotLocale;
52
+ }
53
+ function d(e) {
54
+ if (e instanceof Date) return Number.isNaN(e.getTime()) ? null : e;
55
+ if (typeof e != "string" || e === "") return null;
56
+ const n = new Date(e);
57
+ return Number.isNaN(n.getTime()) ? null : n;
58
+ }
59
+ async function S(e, n, t = {}) {
60
+ if (!h(e)) return { snapshot: null, reason: "malformed_token" };
61
+ const o = t.now ?? (() => /* @__PURE__ */ new Date());
62
+ let s;
63
+ try {
64
+ const { store: r, fields: a } = await y(t);
65
+ s = await N(r, e, n, a);
66
+ } catch {
67
+ return { snapshot: null, reason: "query_error" };
68
+ }
69
+ return f(s, o());
70
+ }
71
+ async function T(e, n, t = {}) {
72
+ return (await S(e, n, t)).snapshot;
73
+ }
74
+ async function N(e, n, t, o) {
75
+ const s = e.openSession();
76
+ try {
77
+ const r = m(o);
78
+ let a = s.query({ indexName: i }).whereEquals("snapshotToken", n).whereEquals("status", "snapshot");
79
+ t && (a = a.whereEquals("locale", t));
80
+ const l = await a.selectFields(r).take(t ? 2 : 64).all();
81
+ return l.length > 0 || !t ? l : (await s.query({ indexName: i }).whereEquals("snapshotToken", n).whereEquals("status", "snapshot").selectFields(r).take(64).all()).filter(u);
82
+ } finally {
83
+ s.dispose();
84
+ }
85
+ }
86
+ function m(e) {
87
+ return [.../* @__PURE__ */ new Set([...p, ...e ?? []])];
88
+ }
89
+ async function y(e) {
90
+ if (e.store) return { store: e.store, fields: e.fields };
91
+ const n = await import("strife:store");
92
+ return { store: n.store, fields: e.fields ?? n.snapshotOptions?.fields };
93
+ }
94
+ export {
95
+ _ as MAX_SNAPSHOT_LOCALE_ROWS,
96
+ i as SNAPSHOT_INDEX_NAME,
97
+ p as SNAPSHOT_ROW_FIELDS,
98
+ c as SNAPSHOT_TOKEN_PATTERN,
99
+ f as classifySnapshotRows,
100
+ h as isSnapshotToken,
101
+ S as lookupSnapshot,
102
+ w as matchSnapshotPath,
103
+ T as resolveSnapshot
104
+ };
@@ -97,9 +97,9 @@ function n(o) {
97
97
  // longer deploys the index or seeds templates.
98
98
  export { store };
99
99
 
100
- // Non-secret share-link options (@strifeapp/astro/share reads the projection
101
- // fields from here when the auto-registered middleware resolves a share).
102
- export const shareOptions = defaultConfig.share || {};
100
+ // Non-secret snapshot-link options (@strifeapp/astro/snapshot reads the projection
101
+ // fields from here when the auto-registered middleware resolves a snapshot).
102
+ export const snapshotOptions = defaultConfig.snapshot || {};
103
103
  `;
104
104
  }
105
105
  };
@@ -1,4 +1,4 @@
1
- import { v as t } from "./vite-plugin-strife-store-Ch0cgrwA.js";
1
+ import { v as t } from "./vite-plugin-strife-store-B5tSnH_D.js";
2
2
  export {
3
3
  t as vitePluginStrifeStore
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strifeapp/astro",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Official Strife integration for Astro — connect your Astro site to a RavenDB-backed Strife content store via a strife:store virtual module.",
5
5
  "keywords": [
6
6
  "astro-integration",
@@ -39,13 +39,13 @@
39
39
  "types": "./dist/edit-mode-middleware.d.ts",
40
40
  "import": "./dist/edit-mode-middleware.js"
41
41
  },
42
- "./share": {
43
- "types": "./dist/share.d.ts",
44
- "import": "./dist/share.js"
42
+ "./snapshot": {
43
+ "types": "./dist/snapshot.d.ts",
44
+ "import": "./dist/snapshot.js"
45
45
  },
46
- "./share-middleware": {
47
- "types": "./dist/share-middleware.d.ts",
48
- "import": "./dist/share-middleware.js"
46
+ "./snapshot-middleware": {
47
+ "types": "./dist/snapshot-middleware.d.ts",
48
+ "import": "./dist/snapshot-middleware.js"
49
49
  },
50
50
  "./LivePreview.astro": "./dist/LivePreview.astro",
51
51
  "./Insights.astro": "./dist/Insights.astro",
@@ -1,36 +0,0 @@
1
- import { APIContext, MiddlewareHandler } from 'astro';
2
- import { ResolveShareOptions } from './share';
3
- /**
4
- * Share-link middleware (STR-2165). Registered `pre` by the integration (see
5
- * src/index.ts) unless `share.middleware` is `false`; also exported as
6
- * `createShareMiddleware` so a site that resolves its locale in its own middleware
7
- * can compose it there with `resolveLocale`.
8
- *
9
- * For a share path (`/share/{guid}` after an optional locale root prefix, where the
10
- * tail is a valid guid — 32 lowercase hex characters) it looks the snapshot up for
11
- * the site's resolved locale and:
12
- * - on a hit sets `Astro.locals.share = { docId, name, expiresAt, locale, content }`
13
- * and lets the site's `src/pages/share/[guid].astro` render it;
14
- * - on any miss renders the site's 404 page through Astro's rewrite, re-wrapped so
15
- * the status is 404 (Astro answers a rewrite with 200).
16
- * Both carry `X-Robots-Tag: noindex, nofollow`, `Cache-Control: private, no-store`
17
- * and `Referrer-Policy: no-referrer`. Edit mode is forced off on share routes, so
18
- * `<LivePreview />` never activates there. Nothing is logged on a hit, and the guid
19
- * is never logged. Every other path passes through untouched — including a
20
- * share-shaped path whose tail is not a guid (`/help/share/link`): as in the .NET
21
- * resolver, that is not a share request but an ordinary site route.
22
- */
23
- export interface ShareMiddlewareOptions extends ResolveShareOptions {
24
- /**
25
- * Resolve the locale of the current request. Defaults to Astro's
26
- * `context.currentLocale` (i18n routing). A site that resolves its locale itself
27
- * — from a cookie, a header, its own path scheme — must pass this, or a
28
- * multi-locale share resolves the wrong (or no) row.
29
- */
30
- resolveLocale?: (context: APIContext) => string | undefined | Promise<string | undefined>;
31
- }
32
- export declare const SHARE_RESPONSE_HEADERS: Readonly<Record<string, string>>;
33
- export declare function createShareMiddleware(options?: ShareMiddlewareOptions): MiddlewareHandler;
34
- /** The default instance the integration registers via `addMiddleware`. */
35
- export declare const onRequest: MiddlewareHandler;
36
- //# sourceMappingURL=share-middleware.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"share-middleware.d.ts","sourceRoot":"","sources":["../src/share-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAA4C,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3F;AAED,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIlE,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,iBAAiB,CA0B7F;AAED,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,iBAA2C,CAAC"}
@@ -1,49 +0,0 @@
1
- import { matchSharePath as u, isShareGuid as i, lookupShare as h } from "./share.js";
2
- const d = Object.freeze({
3
- "X-Robots-Tag": "noindex, nofollow",
4
- "Cache-Control": "private, no-store",
5
- "Referrer-Policy": "no-referrer"
6
- });
7
- function f(r = {}) {
8
- return async (e, a) => {
9
- const t = u(e.url.pathname);
10
- if (t === null || !i(t)) return a();
11
- e.locals.editMode = !1;
12
- const o = r.resolveLocale ? await r.resolveLocale(e) : e.currentLocale, n = await h(t, o, r);
13
- return n.share === null ? (n.reason === "query_error" && console.warn("[strife] share lookup failed (reason=query_error); answering 404"), w(a)) : (e.locals.share = n.share, l(await a()));
14
- };
15
- }
16
- const S = f();
17
- async function w(r) {
18
- let e;
19
- try {
20
- e = await r("/404");
21
- } catch {
22
- e = new Response(null, { status: 404 });
23
- }
24
- return l(e, 404);
25
- }
26
- function l(r, e) {
27
- const a = e !== void 0 && r.status !== e ? s(r, e) : r, t = (o) => {
28
- for (const [n, c] of Object.entries(d))
29
- o.set(n, c);
30
- };
31
- try {
32
- return t(a.headers), a;
33
- } catch {
34
- const o = s(a, a.status);
35
- return t(o.headers), o;
36
- }
37
- }
38
- function s(r, e) {
39
- return new Response(r.body, {
40
- status: e,
41
- statusText: r.statusText,
42
- headers: new Headers(r.headers)
43
- });
44
- }
45
- export {
46
- d as SHARE_RESPONSE_HEADERS,
47
- f as createShareMiddleware,
48
- S as onRequest
49
- };
package/dist/share.d.ts DELETED
@@ -1,126 +0,0 @@
1
- import { IDocumentStore } from 'ravendb';
2
- /**
3
- * Share links (STR-2165): resolve a `/share/{guid}` request to the one live share
4
- * snapshot its guid names, reading the `Content/ByUrl` projection for the resolved
5
- * locale. Locale handling mirrors the .NET SDK's `DefaultRouteResolver` exactly:
6
- * - with a resolved locale the query carries a locale clause; when it yields no row
7
- * (a cookie- or Accept-Language-driven locale the draft never had, a link pasted
8
- * under another prefix) the guid is re-queried without the clause (bounded to
9
- * `MAX_SHARE_LOCALE_ROWS`) and only the snapshot's own row — `locale` equal to
10
- * its `shareLocale` — is kept, so the preview is served rather than hidden;
11
- * - without a resolved locale the guid-only query pages every row of the snapshot
12
- * (again bounded to `MAX_SHARE_LOCALE_ROWS`, never just the first two), and more
13
- * than one candidate row is decided by the same rule: the `shareLocale` row wins,
14
- * and anything other than exactly one such row is an `ambiguous_locale` miss.
15
- * Pure with respect to Astro — the store is injectable, so the lookup is
16
- * unit-testable without RavenDB; `resolveShare` without a store reads the
17
- * integration's `strife:store` virtual module.
18
- *
19
- * The guid is the bearer secret of the link: nothing in this module logs it, and
20
- * a thrown query (an index build that predates share links, an unreachable store)
21
- * is a miss, never an exception that could surface it on an error page.
22
- */
23
- /** The content index the integration queries; deployed by `strife push`. */
24
- export declare const SHARE_INDEX_NAME = "Content/ByUrl";
25
- /** A share guid is exactly 32 lowercase hex characters — nothing looser. */
26
- export declare const SHARE_GUID_PATTERN: RegExp;
27
- /**
28
- * Upper bound on locales one snapshot can carry rows for; a guid-only query (no
29
- * resolved locale, or the locale fallback) never scans further. Same constant as the
30
- * .NET resolver's `MaxShareLocaleRows`.
31
- */
32
- export declare const MAX_SHARE_LOCALE_ROWS = 64;
33
- /**
34
- * Row fields every share lookup projects from the index: the share metadata plus
35
- * what a page needs to pick a layout (`collection`, `displayName`, `url`). `deleted`
36
- * is not stored in the index; RavenDB's default projection behaviour reads it from
37
- * the document, which is exactly the soft-delete check we want. Site-specific
38
- * template fields come from the `fields` option.
39
- */
40
- export declare const SHARE_ROW_FIELDS: readonly ["docId", "locale", "collection", "displayName", "url", "status", "shareGuid", "shareName", "expiresAt", "shareLocale", "createdAt", "changedAt", "publishedAt", "origin", "labels", "deleted"];
41
- /**
42
- * The `Content/ByUrl` row for the snapshot in the resolved locale — the same
43
- * projected, locale-translated object a page gets when it queries the index, plus
44
- * any site fields passed through `fields`.
45
- */
46
- export interface ShareContent {
47
- docId: string;
48
- locale: string;
49
- status: 'share';
50
- shareGuid: string;
51
- shareName?: string | null;
52
- expiresAt: string;
53
- /** The locale the editor was viewing when sharing — the one the link targets. */
54
- shareLocale?: string | null;
55
- collection?: string | null;
56
- displayName?: string | null;
57
- url?: string | null;
58
- [field: string]: unknown;
59
- }
60
- /** What a share page sees on `Astro.locals.share`. */
61
- export interface ShareContext {
62
- /** The snapshot's document id (`{canonicalId}/share/{guid}`). Never log it. */
63
- docId: string;
64
- /** The editor-given name, or `null`. Render as text only — never `set:html`. */
65
- name: string | null;
66
- /** UTC instant after which the link stops resolving. */
67
- expiresAt: Date;
68
- /** The locale the row was resolved for. */
69
- locale: string;
70
- /** The index projection for that locale; render this. */
71
- content: ShareContent;
72
- }
73
- export type ShareMissReason = 'malformed_guid' | 'not_found' | 'ambiguous_locale' | 'deleted' | 'no_expiry' | 'expired' | 'query_error';
74
- export type ShareLookup = {
75
- share: ShareContext;
76
- reason: null;
77
- } | {
78
- share: null;
79
- reason: ShareMissReason;
80
- };
81
- export interface ResolveShareOptions {
82
- /** The document store to query. Defaults to the integration's `strife:store`. */
83
- store?: IDocumentStore;
84
- /**
85
- * Extra index fields to project onto `content` (your template fields, e.g.
86
- * `['heading', 'body']`), so one lookup both decides the 404 and feeds the render.
87
- * Defaults to the integration's `share.fields` option.
88
- */
89
- fields?: readonly string[];
90
- /** Clock, for tests. */
91
- now?: () => Date;
92
- }
93
- export declare function isShareGuid(value: unknown): value is string;
94
- /**
95
- * Matches the share convention `/share/{guid}` after an optional single locale root
96
- * prefix segment (`/sv/share/{guid}`, `/svenska/share/{guid}`), with or without a
97
- * trailing slash. Returns the raw guid segment — validate it with `isShareGuid` — or
98
- * `null` when the path is not share-shaped. The prefix is opaque: the locale comes
99
- * from the site's own resolution, never from the path.
100
- */
101
- export declare function matchSharePath(pathname: string): string | null;
102
- /**
103
- * Picks the single live snapshot row or names the miss. Mirrors the .NET SDK's
104
- * `DefaultRouteResolver.ClassifyShareRows` so both site SDKs agree on "live": with
105
- * more than one row (several locales of one snapshot and no locale resolver to pick
106
- * between them) the share's own locale wins — the row whose `locale` equals its
107
- * `shareLocale` — and anything other than exactly one such row is `ambiguous_locale`.
108
- */
109
- export declare function classifyShareRows(rows: readonly Record<string, unknown>[], now: Date): ShareLookup;
110
- /**
111
- * Looks a share up and reports the outcome with its miss reason. A malformed guid
112
- * never reaches the store. `locale` is the site's resolved locale: the query carries
113
- * it as a clause, and when that yields no row the guid is re-queried without the
114
- * clause and only the snapshot's own `shareLocale` row is kept (the .NET resolver's
115
- * locale fallback). When `locale` is `undefined` (a single-locale site without Astro
116
- * i18n) the clause is omitted from the start and the query pages every row of the
117
- * snapshot; more than one row then resolves to the `shareLocale` row, or is an
118
- * `ambiguous_locale` miss (see `classifyShareRows`).
119
- */
120
- export declare function lookupShare(guid: string, locale: string | undefined, options?: ResolveShareOptions): Promise<ShareLookup>;
121
- /**
122
- * Resolves `guid` in `locale` to the share context, or `null` on any miss
123
- * (unknown, malformed, deleted, expired, missing expiry, query failure).
124
- */
125
- export declare function resolveShare(guid: string, locale: string | undefined, options?: ResolveShareOptions): Promise<ShareContext | null>;
126
- //# sourceMappingURL=share.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAEhD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAAmB,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,0MAiBnB,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,wDAAwD;IACxD,SAAS,EAAE,IAAI,CAAC;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,WAAW,GACX,kBAAkB,GAClB,SAAS,GACT,WAAW,GACX,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,WAAW,CA2BlG;AAcD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,WAAW,CAAC,CAetB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAE9B"}
package/dist/share.js DELETED
@@ -1,104 +0,0 @@
1
- const i = "Content/ByUrl", c = /^[0-9a-f]{32}$/, E = 64, h = [
2
- "docId",
3
- "locale",
4
- "collection",
5
- "displayName",
6
- "url",
7
- "status",
8
- "shareGuid",
9
- "shareName",
10
- "expiresAt",
11
- "shareLocale",
12
- "createdAt",
13
- "changedAt",
14
- "publishedAt",
15
- "origin",
16
- "labels",
17
- "deleted"
18
- ];
19
- function f(e) {
20
- return typeof e == "string" && c.test(e);
21
- }
22
- function p(e) {
23
- const r = /^\/(?:[^/]+\/)?share\/([^/]+)\/?$/.exec(e);
24
- return r ? r[1] : null;
25
- }
26
- function d(e, r) {
27
- if (e.length === 0) return { share: null, reason: "not_found" };
28
- let t = e[0];
29
- if (e.length > 1) {
30
- const a = e.filter(u);
31
- if (a.length !== 1) return { share: null, reason: "ambiguous_locale" };
32
- t = a[0];
33
- }
34
- if (t.deleted === !0) return { share: null, reason: "deleted" };
35
- const s = m(t.expiresAt);
36
- if (s === null) return { share: null, reason: "no_expiry" };
37
- if (s.getTime() <= r.getTime()) return { share: null, reason: "expired" };
38
- const n = t;
39
- return {
40
- share: {
41
- docId: String(n.docId),
42
- name: typeof n.shareName == "string" ? n.shareName : null,
43
- expiresAt: s,
44
- locale: String(n.locale),
45
- content: n
46
- },
47
- reason: null
48
- };
49
- }
50
- function u(e) {
51
- return e.shareLocale != null && e.locale === e.shareLocale;
52
- }
53
- function m(e) {
54
- if (e instanceof Date) return Number.isNaN(e.getTime()) ? null : e;
55
- if (typeof e != "string" || e === "") return null;
56
- const r = new Date(e);
57
- return Number.isNaN(r.getTime()) ? null : r;
58
- }
59
- async function y(e, r, t = {}) {
60
- if (!f(e)) return { share: null, reason: "malformed_guid" };
61
- const s = t.now ?? (() => /* @__PURE__ */ new Date());
62
- let n;
63
- try {
64
- const { store: a, fields: l } = await _(t);
65
- n = await A(a, e, r, l);
66
- } catch {
67
- return { share: null, reason: "query_error" };
68
- }
69
- return d(n, s());
70
- }
71
- async function g(e, r, t = {}) {
72
- return (await y(e, r, t)).share;
73
- }
74
- async function A(e, r, t, s) {
75
- const n = e.openSession();
76
- try {
77
- const a = S(s);
78
- let l = n.query({ indexName: i }).whereEquals("shareGuid", r).whereEquals("status", "share");
79
- t && (l = l.whereEquals("locale", t));
80
- const o = await l.selectFields(a).take(t ? 2 : 64).all();
81
- return o.length > 0 || !t ? o : (await n.query({ indexName: i }).whereEquals("shareGuid", r).whereEquals("status", "share").selectFields(a).take(64).all()).filter(u);
82
- } finally {
83
- n.dispose();
84
- }
85
- }
86
- function S(e) {
87
- return [.../* @__PURE__ */ new Set([...h, ...e ?? []])];
88
- }
89
- async function _(e) {
90
- if (e.store) return { store: e.store, fields: e.fields };
91
- const r = await import("strife:store");
92
- return { store: r.store, fields: e.fields ?? r.shareOptions?.fields };
93
- }
94
- export {
95
- E as MAX_SHARE_LOCALE_ROWS,
96
- c as SHARE_GUID_PATTERN,
97
- i as SHARE_INDEX_NAME,
98
- h as SHARE_ROW_FIELDS,
99
- d as classifyShareRows,
100
- f as isShareGuid,
101
- y as lookupShare,
102
- p as matchSharePath,
103
- g as resolveShare
104
- };