@strifeapp/astro 1.4.1 → 1.5.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 CHANGED
@@ -11,6 +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
15
  - [TypeScript: typing `strife:store`](#typescript-typing-strifestore)
15
16
  - [Direct Vite plugin](#direct-vite-plugin)
16
17
  - [Configuration](#configuration)
@@ -100,6 +101,69 @@ On every page load it reports Core Web Vitals (LCP, INP, CLS, FCP, TTFB) and, wh
100
101
 
101
102
  > The API only stores a beacon when its `Origin` is one of the team's registered domains. For local development, run the API in `Development` and add `localhost` to your team's `Domains`.
102
103
 
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
+
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.
107
+
108
+ ### Share links
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.
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`:
113
+
114
+ ```astro
115
+ ---
116
+ export const prerender = false; // rendered on demand — never prebuilt
117
+ import Layout from '../../layouts/Layout.astro';
118
+
119
+ const { name, expiresAt, locale, content } = Astro.locals.share!;
120
+ ---
121
+ <Layout title={content.displayName ?? 'Preview'}>
122
+ <aside role="status">{name ? `Preview “${name}”` : 'Preview'} — expires {expiresAt.toISOString()}</aside>
123
+ <h1>{content.heading}</h1>
124
+ </Layout>
125
+ ```
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:
128
+
129
+ ```typescript
130
+ strife({
131
+ share: {
132
+ fields: ['heading', 'body'], // your template fields, projected onto Astro.locals.share.content
133
+ },
134
+ });
135
+ ```
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')`).
138
+
139
+ | Option | Where | Description |
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`. |
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:
146
+
147
+ ```typescript
148
+ // src/middleware.ts
149
+ import { sequence } from 'astro:middleware';
150
+ import { createShareMiddleware } from '@strifeapp/astro/share-middleware';
151
+
152
+ export const onRequest = sequence(
153
+ myLocaleMiddleware,
154
+ createShareMiddleware({ resolveLocale: (context) => context.locals.locale }),
155
+ );
156
+ ```
157
+
158
+ Keep these in mind:
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.
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.
164
+
165
+ `resolveShare(guid, locale)` from `@strifeapp/astro/share` is the same lookup as a function, returning the share context or `null`.
166
+
103
167
  ### TypeScript: typing `strife:store`
104
168
 
105
169
  Typed ambient declarations for the `strife:store` virtual module are not bundled in this release. Until a typed surface is published, add your own declaration (e.g. in `src/env.d.ts`):
@@ -24,6 +24,7 @@
24
24
  // consumer's runtime/build (see the integration's env schema).
25
25
  import { getSecret } from 'astro:env/server';
26
26
  import { decodeSecrets } from '@strifeapp/strife/secrets';
27
+ import { insightsMode } from './insights-mode.js';
27
28
 
28
29
  export interface Props {
29
30
  /** Override the collection endpoint. Defaults to the hosted Strife API. */
@@ -62,29 +63,31 @@ function resolveSiteId(): string | null {
62
63
 
63
64
  const site = resolveSiteId();
64
65
 
65
- // `editMode` is true inside the Strife live preview (set by the edit-mode
66
- // middleware). We split the work by audience: Web Vitals run only for REAL visitors
67
- // (they need real-user device/network/interaction variance), while the heavy
68
- // axe-core accessibility audit runs only in the editor's live preview a11y is
69
- // deterministic per content version, so the editor is the right place for it, and it
70
- // keeps axe-core off every real visitor entirely. Without the middleware, editMode
71
- // defaults to false, so plain sites get Web Vitals exactly as before.
72
- const editMode = Astro.locals.editMode ?? false;
66
+ // The middleware-set locals decide the audience. We split the work: Web Vitals run
67
+ // only for REAL visitors (they need real-user device/network/interaction variance),
68
+ // while the heavy axe-core accessibility audit runs only in the editor's live
69
+ // preview (`editMode`) a11y is deterministic per content version, so the editor is
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
73
+ // the middleware, plain sites get Web Vitals exactly as before.
74
+ const mode = insightsMode(Astro.locals);
73
75
  ---
74
76
 
75
- {site && (
77
+ {site && mode !== 'off' && (
76
78
  <Fragment>
77
- <script is:inline define:vars={{ site, endpoint, accessibility, editMode }}>
78
- window.__strifeInsights = { site, endpoint, accessibility, editMode };
79
+ <script is:inline define:vars={{ site, endpoint, accessibility, mode }}>
80
+ window.__strifeInsights = { site, endpoint, accessibility, mode };
79
81
  </script>
80
82
  <script>
81
83
  import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
84
+ import type { InsightsMode } from './insights-mode.js';
82
85
 
83
86
  const cfg = (window as any).__strifeInsights || {};
84
87
  const ENDPOINT: string = cfg.endpoint;
85
88
  const SITE: string = cfg.site;
86
89
  const RUN_A11Y: boolean = cfg.accessibility !== false;
87
- const EDIT_MODE: boolean = cfg.editMode === true;
90
+ const MODE: InsightsMode = cfg.mode;
88
91
 
89
92
  /** Beacon a payload to the collection endpoint (fire-and-forget). */
90
93
  function send(payload: Record<string, unknown>) {
@@ -231,17 +234,18 @@ const editMode = Astro.locals.editMode ?? false;
231
234
  scheduleAccessibilityAudit();
232
235
  }
233
236
 
234
- if (EDIT_MODE) {
237
+ if (MODE === 'audit') {
235
238
  // In the Strife live preview: only the accessibility audit runs. The editor is
236
239
  // the right place for the heavy axe-core scan (a11y is deterministic per content
237
240
  // version), and it keeps axe-core off every real visitor.
238
241
  auditOnce();
239
242
  // Astro View Transitions: re-audit the newly loaded page.
240
243
  document.addEventListener('astro:page-load', auditOnce);
241
- } else {
244
+ } else if (MODE === 'vitals') {
242
245
  // A real visitor: Web Vitals only, batched on pagehide. No axe-core download/run.
243
246
  initWebVitals();
244
247
  }
248
+ // 'off' (a share link) never reaches this script: the component renders nothing.
245
249
  </script>
246
250
  </Fragment>
247
251
  )}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,31 @@
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
5
  export interface IntegrationOptions extends IAuthOptions {
4
6
  certificate?: string;
5
7
  password?: string;
6
8
  urls?: string[];
7
9
  database?: string;
8
10
  collections?: Collection[];
11
+ /** Share links (`/share/{guid}`); see `@strifeapp/astro/share-middleware`. */
12
+ share?: ShareOptions;
13
+ }
14
+ export interface ShareOptions {
15
+ /**
16
+ * Index fields to project onto `Astro.locals.share.content` beside the row fields
17
+ * (your template fields, e.g. `['heading', 'body']`), so the middleware's one
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`.
20
+ */
21
+ fields?: string[];
22
+ /**
23
+ * `false` skips the automatic `pre` registration of the share middleware. Do this
24
+ * when your site resolves its locale in its own middleware: compose
25
+ * `createShareMiddleware({ resolveLocale })` from `@strifeapp/astro/share-middleware`
26
+ * in `src/middleware.ts` instead.
27
+ */
28
+ middleware?: boolean;
9
29
  }
10
30
  export interface Collection {
11
31
  name: string;
@@ -21,6 +41,14 @@ declare global {
21
41
  * e.g. render draft vs published content.
22
42
  */
23
43
  editMode?: boolean;
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 —
47
+ * never `set:html`), when the link expires, the resolved locale, and the
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).
50
+ */
51
+ share?: ShareContext;
24
52
  }
25
53
  }
26
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;AAI5C,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;CAC5B;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;SACpB;KACF;CACF;AAMD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,gBAAgB,CAwDxF"}
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"}
package/dist/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  import { envField as e } from "astro/config";
2
- import { v as c } from "./vite-plugin-strife-store-B-B_HXbA.js";
3
- const i = {
2
+ import { v as i } from "./vite-plugin-strife-store-Ch0cgrwA.js";
3
+ const c = {
4
4
  type: "pfx"
5
5
  };
6
- function p(t) {
6
+ function l(t) {
7
7
  return {
8
8
  name: "@strife/astro",
9
9
  hooks: {
10
- "astro:config:setup": ({ updateConfig: r, addMiddleware: s }) => {
11
- const n = {
12
- ...i,
10
+ "astro:config:setup": ({ updateConfig: o, addMiddleware: r }) => {
11
+ const s = {
12
+ ...c,
13
13
  ...t ? Object.fromEntries(
14
- Object.entries(t).filter(([, o]) => o != null)
14
+ Object.entries(t).filter(([, n]) => n != null)
15
15
  ) : {}
16
16
  };
17
- r({
17
+ o({
18
18
  env: {
19
19
  schema: {
20
20
  // Consolidated single-var secret (packs urls/database/certificate/password,
@@ -32,16 +32,19 @@ function p(t) {
32
32
  }
33
33
  },
34
34
  vite: {
35
- plugins: [c(n)]
35
+ plugins: [i(s)]
36
36
  }
37
- }), s({
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",
42
+ order: "pre"
40
43
  });
41
44
  }
42
45
  }
43
46
  };
44
47
  }
45
48
  export {
46
- p as default
49
+ l as default
47
50
  };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Which telemetry `<Insights />` runs for a request, from the middleware-set locals:
3
+ * - `vitals`: a real visitor — Core Web Vitals only;
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.
7
+ * Pure, so the gate is unit-tested; Insights.astro only forwards the result.
8
+ */
9
+ export type InsightsMode = 'vitals' | 'audit' | 'off';
10
+ export declare function insightsMode(locals: {
11
+ editMode?: boolean;
12
+ share?: unknown;
13
+ }): InsightsMode;
14
+ //# sourceMappingURL=insights-mode.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,6 @@
1
+ function e(t) {
2
+ return t.share != null ? "off" : t.editMode === !0 ? "audit" : "vitals";
3
+ }
4
+ export {
5
+ e as insightsMode
6
+ };
@@ -0,0 +1,36 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,49 @@
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
+ };
@@ -0,0 +1,126 @@
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
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1,104 @@
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
+ };
@@ -96,6 +96,10 @@ function n(o) {
96
96
  // operator-controlled path). The store only connects and reads — Astro no
97
97
  // longer deploys the index or seeds templates.
98
98
  export { store };
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 || {};
99
103
  `;
100
104
  }
101
105
  };
@@ -1,4 +1,4 @@
1
- import { v as t } from "./vite-plugin-strife-store-B-B_HXbA.js";
1
+ import { v as t } from "./vite-plugin-strife-store-Ch0cgrwA.js";
2
2
  export {
3
3
  t as vitePluginStrifeStore
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-strife-store.d.ts","sourceRoot":"","sources":["../src/vite-plugin-strife-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAKlD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CA6G9E"}
1
+ {"version":3,"file":"vite-plugin-strife-store.d.ts","sourceRoot":"","sources":["../src/vite-plugin-strife-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAKlD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAiH9E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strifeapp/astro",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
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,6 +39,14 @@
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"
45
+ },
46
+ "./share-middleware": {
47
+ "types": "./dist/share-middleware.d.ts",
48
+ "import": "./dist/share-middleware.js"
49
+ },
42
50
  "./LivePreview.astro": "./dist/LivePreview.astro",
43
51
  "./Insights.astro": "./dist/Insights.astro",
44
52
  "./vite-plugin-strife-store": "./dist/vite-plugin-strife-store-entry.js"
@@ -63,17 +71,17 @@
63
71
  "@strifeapp/strife": "^1.1.0",
64
72
  "axe-core": "^4.11.0",
65
73
  "dotenv": "^17.2.3",
66
- "jose": "^5.9.6",
74
+ "jose": "^6.2.9",
67
75
  "ravendb": "^7.1.4",
68
76
  "serialize-javascript": "^7.0.5",
69
- "web-vitals": "^5.1.0"
77
+ "web-vitals": "^6.1.1"
70
78
  },
71
79
  "devDependencies": {
72
80
  "@types/serialize-javascript": "^5.0.4",
73
- "astro": "^6.3.6",
81
+ "astro": "^6.4.8",
74
82
  "rimraf": "^6.0.1",
75
83
  "typescript": "^5.7.3",
76
- "vite": "^7.0.0",
84
+ "vite": "^7.3.6",
77
85
  "vite-plugin-dts": "^4.5.3",
78
86
  "vitest": "^4.1.7"
79
87
  },