@storystick/client 0.0.0-stage → 0.2.0-rc.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/LICENSE +112 -0
- package/PUBLIC-API.md +22 -0
- package/README.md +180 -2
- package/dist/.source-hash +1 -0
- package/dist/_fetchers.d.ts +51 -0
- package/dist/_fetchers.js +144 -0
- package/dist/_stateMachine.d.ts +102 -0
- package/dist/_stateMachine.js +168 -0
- package/dist/_wireAuth.d.ts +48 -0
- package/dist/_wireAuth.js +52 -0
- package/dist/adapter.d.ts +14 -0
- package/dist/adapter.js +10 -0
- package/dist/content-values.d.ts +11 -0
- package/dist/content-values.js +401 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +45 -0
- package/dist/hydrate-slice.d.ts +2 -0
- package/dist/hydrate-slice.js +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +9 -0
- package/dist/infra-resolver.d.ts +94 -0
- package/dist/infra-resolver.js +130 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +20 -0
- package/dist/locale-routing.d.ts +5 -0
- package/dist/locale-routing.js +33 -0
- package/dist/normalize-pathname.d.ts +79 -0
- package/dist/normalize-pathname.js +103 -0
- package/dist/preview-frame-dependencies.d.ts +39 -0
- package/dist/preview-frame-dependencies.js +84 -0
- package/dist/preview-query-receipt.d.ts +3 -0
- package/dist/preview-query-receipt.js +16 -0
- package/dist/preview-read-lifecycle.d.ts +10 -0
- package/dist/preview-read-lifecycle.js +40 -0
- package/dist/preview-read-observation.d.ts +18 -0
- package/dist/preview-read-observation.js +81 -0
- package/dist/preview-scope.d.ts +3 -0
- package/dist/preview-scope.js +22 -0
- package/dist/preview-target.d.ts +73 -0
- package/dist/preview-target.js +86 -0
- package/dist/project-locales.d.ts +4 -0
- package/dist/project-locales.js +12 -0
- package/dist/public-delivery.d.ts +36 -0
- package/dist/public-delivery.js +55 -0
- package/dist/published-routes.d.ts +3 -0
- package/dist/published-routes.js +100 -0
- package/dist/query-builder.d.ts +225 -0
- package/dist/query-builder.js +294 -0
- package/dist/query-client.d.ts +54 -0
- package/dist/query-client.js +306 -0
- package/dist/query-compiler.d.ts +5 -0
- package/dist/query-compiler.js +12 -0
- package/dist/query-schema.d.ts +32 -0
- package/dist/query-schema.js +1 -0
- package/dist/redirect-search.d.ts +4 -0
- package/dist/redirect-search.js +14 -0
- package/dist/reference-resolver.d.ts +37 -0
- package/dist/reference-resolver.js +126 -0
- package/dist/resolve-route.d.ts +27 -0
- package/dist/resolve-route.js +145 -0
- package/dist/resolve.d.ts +43 -0
- package/dist/resolve.js +160 -0
- package/dist/schemas.d.ts +71 -0
- package/dist/schemas.js +75 -0
- package/dist/seo.d.ts +52 -0
- package/dist/seo.js +93 -0
- package/dist/signed-webhook.d.ts +13 -0
- package/dist/signed-webhook.js +54 -0
- package/dist/transport.d.ts +29 -0
- package/dist/transport.js +584 -0
- package/dist/typed-route.d.ts +22 -0
- package/dist/typed-route.js +1 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +1 -0
- package/package.json +70 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure URL resolution — ADR-131 C9.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes derivation of the three Convex-flavoured URLs the customer-
|
|
5
|
+
* bundled SDK packages need at runtime :
|
|
6
|
+
*
|
|
7
|
+
* - `endpoint` — gateway HTTP root, `.convex.site` form. Hosts the
|
|
8
|
+
* custom HTTP routes (JWKS, Pattern-D queries).
|
|
9
|
+
* - `convexUrl` — standard Convex RPC + WebSocket sync URL, `.convex.cloud`
|
|
10
|
+
* form. Used by `ConvexReactClient` (WS) and the server-
|
|
11
|
+
* side `ConvexHttpClient` (preview session lookup,
|
|
12
|
+
* observation tap mutation).
|
|
13
|
+
* - `jwksUrl` — `${endpoint}/api/jwks.json` by default.
|
|
14
|
+
*
|
|
15
|
+
* Cascade (per URL, independent) :
|
|
16
|
+
*
|
|
17
|
+
* 1. Explicit `options.*Url` — call-site override (props, env vars read
|
|
18
|
+
* by the adapter and forwarded as options, factory args).
|
|
19
|
+
* 2. Auto-derive from `resolveProject(projectId).endpoint`.
|
|
20
|
+
* 3. Fail-loud — `resolveProject` throws a typed `StorystickError` if
|
|
21
|
+
* the gateway returns 404/503/network error.
|
|
22
|
+
*
|
|
23
|
+
* The single `resolveProject` round-trip is shared across the three URLs ;
|
|
24
|
+
* subsequent calls hit the per-process cache (server) or sessionStorage
|
|
25
|
+
* cache (browser).
|
|
26
|
+
*
|
|
27
|
+
* **Env vars are NOT read here.** The bundler-replace behaviour of
|
|
28
|
+
* `process.env.NEXT_PUBLIC_*` requires literal property access in the
|
|
29
|
+
* adapter package (Next/React) so the static replacement at build time
|
|
30
|
+
* resolves the value into the browser bundle. Adapters call this helper
|
|
31
|
+
* after reading their respective env vars and forwarding them as options.
|
|
32
|
+
*
|
|
33
|
+
* Direction produit (owner 2026-05-26 PM) :
|
|
34
|
+
*
|
|
35
|
+
* > "Convex doit rester transparent au dev. Hors de question qu'une env
|
|
36
|
+
* > var mentionnant convex.cloud / .site / etc apparaisse côté customer."
|
|
37
|
+
*
|
|
38
|
+
* The 5-var customer allowlist is `STORYSTICK_PROJECT_ID`, `STORYSTICK_API_KEY`,
|
|
39
|
+
* `NEXT_PUBLIC_STORYSTICK_PROJECT_ID`, `NEXT_PUBLIC_STORYSTICK_PUBLISHABLE_KEY`,
|
|
40
|
+
* `NEXT_PUBLIC_STORYSTICK_STUDIO_ORIGIN`. All Convex-flavoured URLs are
|
|
41
|
+
* derived from `projectId` via this module.
|
|
42
|
+
* Resource-storage bucket credentials belong exclusively to the server-side
|
|
43
|
+
* storage resolver; they are never customer env vars or resolver options.
|
|
44
|
+
*/
|
|
45
|
+
import { resolveProject } from './resolve.js';
|
|
46
|
+
const JWKS_PATH = '/api/jwks.json';
|
|
47
|
+
/**
|
|
48
|
+
* Swap `.convex.site` → `.convex.cloud` on a Convex deployment URL. Returns
|
|
49
|
+
* the input unchanged for non-Convex-Cloud deployments (self-host) — the
|
|
50
|
+
* customer sets `convexUrl` explicitly in that case.
|
|
51
|
+
*
|
|
52
|
+
* **CSO trade-off (codex R2 P1, accepted per ADR-131 C9 brief direction
|
|
53
|
+
* produit) :** the default `convexUrl` resolves to a raw `.convex.cloud`
|
|
54
|
+
* deployment URL, bypassing the sub-gateway Worker (`services/sub-gateway/`).
|
|
55
|
+
* Pre-C9, the default was `http://localhost:8787` (dev Worker URL) and
|
|
56
|
+
* customers in prod were expected to set
|
|
57
|
+
* `NEXT_PUBLIC_STORYSTICK_GATEWAY_SYNC_URL` explicitly — usually to the
|
|
58
|
+
* Worker public URL (e.g. `https://gateway.storystick.app`). The Worker
|
|
59
|
+
* is not deployed yet (5 Path B manual steps in
|
|
60
|
+
* `services/sub-gateway/wrangler.toml`), so the spike-Vercel Fix 2 already
|
|
61
|
+
* documents `.convex.cloud` direct as the only working posture today.
|
|
62
|
+
*
|
|
63
|
+
* Once the Worker is deployed, replace this default with the Worker
|
|
64
|
+
* public URL constant ; the rest of the cascade is unchanged. The
|
|
65
|
+
* explicit `options.convexUrl` override remains the escape hatch for
|
|
66
|
+
* customers needing to bypass the constant (self-host, internal Vercel
|
|
67
|
+
* deploys, etc.).
|
|
68
|
+
*
|
|
69
|
+
* See `storystick-hq/notes/adr-131-chantier/c9-hidden-vars-audit.md` §9
|
|
70
|
+
* for the deferred-follow-up list.
|
|
71
|
+
*/
|
|
72
|
+
function gatewayToConvex(endpoint) {
|
|
73
|
+
if (endpoint.includes('.convex.site')) {
|
|
74
|
+
return endpoint.replace('.convex.site', '.convex.cloud');
|
|
75
|
+
}
|
|
76
|
+
return endpoint;
|
|
77
|
+
}
|
|
78
|
+
function stripTrailingSlash(url) {
|
|
79
|
+
return url.replace(/\/$/, '');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Synchronously derive `ResolvedInfra` when `endpoint` is already known
|
|
83
|
+
* (explicit prop or env override). Used by adapters that need an
|
|
84
|
+
* initial value during render — e.g. `<StorystickQueryProvider>`
|
|
85
|
+
* synchronously seeds its `infra` state when all required values are
|
|
86
|
+
* explicit, so generated clients can execute on the first render
|
|
87
|
+
* inside an immediately-available `ConvexProvider` (codex R2 P2 fix).
|
|
88
|
+
*
|
|
89
|
+
* Returns `null` when `endpoint` is missing (gateway round-trip needed).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveInfraSync(options = {}) {
|
|
92
|
+
if (options.endpoint === undefined)
|
|
93
|
+
return null;
|
|
94
|
+
const endpoint = stripTrailingSlash(options.endpoint);
|
|
95
|
+
const convexUrl = stripTrailingSlash(options.convexUrl ?? gatewayToConvex(endpoint));
|
|
96
|
+
const jwksUrl = options.jwksUrl !== undefined ? options.jwksUrl : `${endpoint}${JWKS_PATH}`;
|
|
97
|
+
return { endpoint, convexUrl, jwksUrl };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the three infrastructure URLs for `projectId`.
|
|
101
|
+
*
|
|
102
|
+
* Idempotent + cache-aware — repeated calls with the same `projectId` reuse
|
|
103
|
+
* the `resolveProject` cache (per-process Map server-side, sessionStorage
|
|
104
|
+
* browser-side). Safe to call eagerly at module init.
|
|
105
|
+
*
|
|
106
|
+
* The `resolveProject` round-trip happens only when no explicit endpoint
|
|
107
|
+
* override is supplied — `convexUrl` + `jwksUrl` are pure transforms on
|
|
108
|
+
* `endpoint`. If `endpoint` is provided (prop or env escape hatch), the
|
|
109
|
+
* helper completes without any network call ; gateway failures cannot
|
|
110
|
+
* stall the boot path in that case.
|
|
111
|
+
*
|
|
112
|
+
* Throws (propagates) the `StorystickError` from `resolveProject` when the
|
|
113
|
+
* gateway returns 404/503/network error AND no explicit `endpoint` is set.
|
|
114
|
+
*/
|
|
115
|
+
export async function resolveInfra(projectId, options = {}) {
|
|
116
|
+
// The gateway round-trip is needed only when we don't already know the
|
|
117
|
+
// endpoint — convexUrl + jwksUrl are pure transforms on it. Explicit
|
|
118
|
+
// endpoint prop alone is enough to skip the round-trip.
|
|
119
|
+
let endpointSource = options.endpoint;
|
|
120
|
+
if (endpointSource === undefined) {
|
|
121
|
+
const config = await resolveProject(projectId, options.gatewayUrl);
|
|
122
|
+
endpointSource = config.endpoint;
|
|
123
|
+
}
|
|
124
|
+
const endpoint = stripTrailingSlash(endpointSource);
|
|
125
|
+
const convexUrl = stripTrailingSlash(options.convexUrl ?? gatewayToConvex(endpoint));
|
|
126
|
+
const jwksUrl = options.jwksUrl !== undefined
|
|
127
|
+
? options.jwksUrl
|
|
128
|
+
: `${endpoint}${JWKS_PATH}`;
|
|
129
|
+
return { endpoint, convexUrl, jwksUrl };
|
|
130
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal entry point — Storystick surfaces only (ADR-045 §11.2).
|
|
3
|
+
*
|
|
4
|
+
* Consumers outside `apps/studio`, `apps/ops`, `apps/hub` should NOT
|
|
5
|
+
* import from here. This file exposes the user-session fetcher (which
|
|
6
|
+
* the public SDK doesn't need) and direct access to the state machine.
|
|
7
|
+
*/
|
|
8
|
+
export { wireConvexAuth } from './_wireAuth.js';
|
|
9
|
+
export { assertExactQueryResult } from './errors.js';
|
|
10
|
+
export type { ConvexClientWithSetAuth, WireAuthOptions, WireAuthResult, } from './_wireAuth.js';
|
|
11
|
+
export { userSessionFetcher, apiKeyFetcher } from './_fetchers.js';
|
|
12
|
+
export type { UserSessionFetcherOptions, ApiKeyFetcherOptions, } from './_fetchers.js';
|
|
13
|
+
export { createTokenStateMachine, isHardAuthError, isRefreshableError, } from './_stateMachine.js';
|
|
14
|
+
export type { SdkError, StateMachineErrorCode, TokenFetcher, TokenFetchResult, TokenStateMachine, } from './_stateMachine.js';
|
|
15
|
+
export { getExecutableQueryDescriptor } from './query-builder.js';
|
|
16
|
+
export type { InternalExecutableQueryDescriptor } from './query-builder.js';
|
|
17
|
+
export { createInternalClient, bindClientSchemaToAuthority, clientMatchesPreviewAuthority, getInternalClientRuntime, } from './query-client.js';
|
|
18
|
+
export type { InternalClientOptions, InternalClientRuntime, InternalPreviewAuthorityScope, } from './query-client.js';
|
|
19
|
+
/** Internal Studio wire knob. Public SDK reads always use backend `explicit`. */
|
|
20
|
+
export interface InternalLocaleWireOptions {
|
|
21
|
+
locale?: string;
|
|
22
|
+
fallbackPolicy?: 'none' | 'explicit';
|
|
23
|
+
}
|
|
24
|
+
export { attachPreviewReadObservation, collectPreviewReadObservations, conservativePreviewRenderWatermark, getPreviewReadObservation, } from './preview-read-observation.js';
|
|
25
|
+
export type { PreviewReadObservation } from './preview-read-observation.js';
|
|
26
|
+
export { getPreviewQueryReceiptId } from './preview-query-receipt.js';
|
|
27
|
+
export { resolvePreviewTarget } from './preview-target.js';
|
|
28
|
+
export type { PreviewTarget } from './preview-target.js';
|
|
29
|
+
export { previewAssetDependenciesFromValue } from './preview-frame-dependencies.js';
|
|
30
|
+
export * from './hydrate-slice.js';
|
|
31
|
+
export * from './content-values.js';
|
|
32
|
+
export { attachPreviewQueryReceipt } from './preview-query-receipt.js';
|
|
33
|
+
export type { RequestContext, PreviewReadScopeEcho, PreviewProvenanceReadLifecycle, PreviewProvenanceReadLifecycleInput, Transport, TransportRequest, } from './types.js';
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal entry point — Storystick surfaces only (ADR-045 §11.2).
|
|
3
|
+
*
|
|
4
|
+
* Consumers outside `apps/studio`, `apps/ops`, `apps/hub` should NOT
|
|
5
|
+
* import from here. This file exposes the user-session fetcher (which
|
|
6
|
+
* the public SDK doesn't need) and direct access to the state machine.
|
|
7
|
+
*/
|
|
8
|
+
export { wireConvexAuth } from './_wireAuth.js';
|
|
9
|
+
export { assertExactQueryResult } from './errors.js';
|
|
10
|
+
export { userSessionFetcher, apiKeyFetcher } from './_fetchers.js';
|
|
11
|
+
export { createTokenStateMachine, isHardAuthError, isRefreshableError, } from './_stateMachine.js';
|
|
12
|
+
export { getExecutableQueryDescriptor } from './query-builder.js';
|
|
13
|
+
export { createInternalClient, bindClientSchemaToAuthority, clientMatchesPreviewAuthority, getInternalClientRuntime, } from './query-client.js';
|
|
14
|
+
export { attachPreviewReadObservation, collectPreviewReadObservations, conservativePreviewRenderWatermark, getPreviewReadObservation, } from './preview-read-observation.js';
|
|
15
|
+
export { getPreviewQueryReceiptId } from './preview-query-receipt.js';
|
|
16
|
+
export { resolvePreviewTarget } from './preview-target.js';
|
|
17
|
+
export { previewAssetDependenciesFromValue } from './preview-frame-dependencies.js';
|
|
18
|
+
export * from './hydrate-slice.js';
|
|
19
|
+
export * from './content-values.js';
|
|
20
|
+
export { attachPreviewQueryReceipt } from './preview-query-receipt.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { publicHrefForPublication, publicHrefForLocale, publishedLocaleHrefs, normalizePublicLocaleOrigins, prefixHref, publicPathnameForLocale, resolveLocalePathname, readDocumentLocaleRouting } from '@storystick/types/internal/locale-routing';
|
|
2
|
+
export type { PublicLocaleOrigins, LocalePathnameResolution, LocaleRouting, LocaleRoutingSettings, RoutingLocale, DocumentLocaleRouting } from '@storystick/types/internal/locale-routing';
|
|
3
|
+
import type { LocaleRoutingSettings } from '@storystick/types/internal/locale-routing';
|
|
4
|
+
/** @internal Attach only after validating the wire response. No credentials or view identity. */
|
|
5
|
+
export declare function attachDocumentLocaleRouting<T>(value: T, settings: LocaleRoutingSettings): T;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export { publicHrefForPublication, publicHrefForLocale, publishedLocaleHrefs, normalizePublicLocaleOrigins, prefixHref, publicPathnameForLocale, resolveLocalePathname, readDocumentLocaleRouting } from '@storystick/types/internal/locale-routing';
|
|
2
|
+
/** @internal Attach only after validating the wire response. No credentials or view identity. */
|
|
3
|
+
export function attachDocumentLocaleRouting(value, settings) {
|
|
4
|
+
const seen = new WeakSet();
|
|
5
|
+
const visit = (item) => {
|
|
6
|
+
if (typeof item !== 'object' || item === null || seen.has(item))
|
|
7
|
+
return;
|
|
8
|
+
seen.add(item);
|
|
9
|
+
if (Array.isArray(item)) {
|
|
10
|
+
item.forEach(visit);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const record = item;
|
|
14
|
+
if (typeof record.documentId === 'string' && typeof record.storyType === 'string' &&
|
|
15
|
+
isRecord(record.fields) && isRecord(record._locale) && typeof record._locale.code === 'string') {
|
|
16
|
+
record._localeRouting = {
|
|
17
|
+
locale: record._locale.code,
|
|
18
|
+
defaultLocale: settings.defaultLocale,
|
|
19
|
+
locales: settings.locales.map(({ code, enabled }) => ({ code, enabled })),
|
|
20
|
+
localeRouting: { ...settings.localeRouting },
|
|
21
|
+
...(settings.publicOrigins ? { publicOrigins: { ...settings.publicOrigins } } : {}),
|
|
22
|
+
...(settings.synthetic === undefined ? {} : { synthetic: settings.synthetic }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
for (const [key, child] of Object.entries(record)) {
|
|
26
|
+
if (key !== '_localeRouting')
|
|
27
|
+
visit(child);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
visit(value);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pathname normalization helpers (ADR-090 §3).
|
|
3
|
+
*
|
|
4
|
+
* The public SDK accepts all developer-natural forms for a pathname:
|
|
5
|
+
*
|
|
6
|
+
* - `undefined` (catch-all root in Next.js: `params.segments` for `/`)
|
|
7
|
+
* - `""`, `"/"` (empty / root)
|
|
8
|
+
* - `"accueil"`, `"/accueil"` (single segment, optional leading slash)
|
|
9
|
+
* - `["blog", "post"]` (array of segments, e.g. Next.js `params.segments`)
|
|
10
|
+
* - `[]` (empty array — homepage)
|
|
11
|
+
*
|
|
12
|
+
* All forms normalize to a canonical internal form: a single string of
|
|
13
|
+
* slug-like segments joined by `"/"`, with no leading or trailing slash.
|
|
14
|
+
* The empty string `""` represents the homepage.
|
|
15
|
+
*
|
|
16
|
+
* | Input | Canonical form |
|
|
17
|
+
* |--------------------|----------------|
|
|
18
|
+
* | `undefined` | `""` |
|
|
19
|
+
* | `""` / `"/"` | `""` |
|
|
20
|
+
* | `"accueil"` | `"accueil"` |
|
|
21
|
+
* | `"/accueil"` | `"accueil"` |
|
|
22
|
+
* | `"/blog/post"` | `"blog/post"` |
|
|
23
|
+
* | `["blog", "post"]` | `"blog/post"` |
|
|
24
|
+
* | `[]` | `""` |
|
|
25
|
+
*
|
|
26
|
+
* The canonical form is what a compiled Query V2 pathname address stores when
|
|
27
|
+
* `.by({ pathname })` is called, and what backend `getByPathname` will
|
|
28
|
+
* eventually accept on the wire (currently routed via list + filter
|
|
29
|
+
* fallback on the `resolvedPathname` allowlist column from ADR-088).
|
|
30
|
+
*
|
|
31
|
+
* These helpers are framework-agnostic and shared by all framework
|
|
32
|
+
* adapters so that normalization behavior is identical across Next.js,
|
|
33
|
+
* Astro, SvelteKit, and any future SDK.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a developer-supplied pathname input to its canonical form.
|
|
37
|
+
*
|
|
38
|
+
* normalizePathname(undefined) // ""
|
|
39
|
+
* normalizePathname("/") // ""
|
|
40
|
+
* normalizePathname("/accueil") // "accueil"
|
|
41
|
+
* normalizePathname(["blog", "post"]) // "blog/post"
|
|
42
|
+
* normalizePathname([]) // ""
|
|
43
|
+
*/
|
|
44
|
+
export declare function normalizePathname(input: string | string[] | undefined): string;
|
|
45
|
+
/**
|
|
46
|
+
* Convert a canonical pathname into segment array form.
|
|
47
|
+
* Inverse of {@link segmentsToPathname}.
|
|
48
|
+
*
|
|
49
|
+
* pathnameToSegments("") // []
|
|
50
|
+
* pathnameToSegments("accueil") // ["accueil"]
|
|
51
|
+
* pathnameToSegments("blog/post") // ["blog", "post"]
|
|
52
|
+
*/
|
|
53
|
+
export declare function pathnameToSegments(pathname: string): string[];
|
|
54
|
+
/**
|
|
55
|
+
* Convert a segment array into canonical pathname string form.
|
|
56
|
+
* Inverse of {@link pathnameToSegments}.
|
|
57
|
+
*
|
|
58
|
+
* segmentsToPathname([]) // ""
|
|
59
|
+
* segmentsToPathname(["accueil"]) // "accueil"
|
|
60
|
+
* segmentsToPathname(["blog", "post"]) // "blog/post"
|
|
61
|
+
*/
|
|
62
|
+
export declare function segmentsToPathname(segments: string[]): string;
|
|
63
|
+
/**
|
|
64
|
+
* Convert a canonical pathname (`'blog/post-1'` or `''` for homepage) to a
|
|
65
|
+
* browser-displayable path with leading slash. Inverse of {@link normalizePathname}.
|
|
66
|
+
*
|
|
67
|
+
* pathnameToHref('') → '/'
|
|
68
|
+
* pathnameToHref('accueil') → '/accueil'
|
|
69
|
+
* pathnameToHref('blog/post-1') → '/blog/post-1'
|
|
70
|
+
*
|
|
71
|
+
* Trusts canonical-form input (no leading slash, `''` for homepage). Producers
|
|
72
|
+
* are expected to call `normalizePathname` first if they hold non-canonical
|
|
73
|
+
* input. This keeps the helper minimal and the canonicalization rule single-sourced.
|
|
74
|
+
*
|
|
75
|
+
* Use cases: ADR-094 catch-all routing (companion utility consumed by customer
|
|
76
|
+
* renderers + the Studio iframe routing in ADR-095), future sitemap generation,
|
|
77
|
+
* AI Builder doc→URL pass, "share my draft" link generators.
|
|
78
|
+
*/
|
|
79
|
+
export declare function pathnameToHref(canonical: string): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { normalizeQueryPathname } from '@storystick/types/pathname-query';
|
|
2
|
+
/**
|
|
3
|
+
* Pathname normalization helpers (ADR-090 §3).
|
|
4
|
+
*
|
|
5
|
+
* The public SDK accepts all developer-natural forms for a pathname:
|
|
6
|
+
*
|
|
7
|
+
* - `undefined` (catch-all root in Next.js: `params.segments` for `/`)
|
|
8
|
+
* - `""`, `"/"` (empty / root)
|
|
9
|
+
* - `"accueil"`, `"/accueil"` (single segment, optional leading slash)
|
|
10
|
+
* - `["blog", "post"]` (array of segments, e.g. Next.js `params.segments`)
|
|
11
|
+
* - `[]` (empty array — homepage)
|
|
12
|
+
*
|
|
13
|
+
* All forms normalize to a canonical internal form: a single string of
|
|
14
|
+
* slug-like segments joined by `"/"`, with no leading or trailing slash.
|
|
15
|
+
* The empty string `""` represents the homepage.
|
|
16
|
+
*
|
|
17
|
+
* | Input | Canonical form |
|
|
18
|
+
* |--------------------|----------------|
|
|
19
|
+
* | `undefined` | `""` |
|
|
20
|
+
* | `""` / `"/"` | `""` |
|
|
21
|
+
* | `"accueil"` | `"accueil"` |
|
|
22
|
+
* | `"/accueil"` | `"accueil"` |
|
|
23
|
+
* | `"/blog/post"` | `"blog/post"` |
|
|
24
|
+
* | `["blog", "post"]` | `"blog/post"` |
|
|
25
|
+
* | `[]` | `""` |
|
|
26
|
+
*
|
|
27
|
+
* The canonical form is what a compiled Query V2 pathname address stores when
|
|
28
|
+
* `.by({ pathname })` is called, and what backend `getByPathname` will
|
|
29
|
+
* eventually accept on the wire (currently routed via list + filter
|
|
30
|
+
* fallback on the `resolvedPathname` allowlist column from ADR-088).
|
|
31
|
+
*
|
|
32
|
+
* These helpers are framework-agnostic and shared by all framework
|
|
33
|
+
* adapters so that normalization behavior is identical across Next.js,
|
|
34
|
+
* Astro, SvelteKit, and any future SDK.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a developer-supplied pathname input to its canonical form.
|
|
38
|
+
*
|
|
39
|
+
* normalizePathname(undefined) // ""
|
|
40
|
+
* normalizePathname("/") // ""
|
|
41
|
+
* normalizePathname("/accueil") // "accueil"
|
|
42
|
+
* normalizePathname(["blog", "post"]) // "blog/post"
|
|
43
|
+
* normalizePathname([]) // ""
|
|
44
|
+
*/
|
|
45
|
+
export function normalizePathname(input) {
|
|
46
|
+
if (input === undefined)
|
|
47
|
+
return '';
|
|
48
|
+
if (Array.isArray(input)) {
|
|
49
|
+
// Filter out empty / falsy segments so `["", "blog"]` doesn't produce
|
|
50
|
+
// a leading-slash canonical form. This also makes `normalizePathname([""])`
|
|
51
|
+
// correctly produce `""` (homepage).
|
|
52
|
+
const filtered = input.filter((s) => typeof s === 'string' && s.length > 0);
|
|
53
|
+
return filtered.join('/');
|
|
54
|
+
}
|
|
55
|
+
// String input: strip leading and trailing slashes.
|
|
56
|
+
// "/" → "" (after strip), "/accueil" → "accueil", "/blog/post/" → "blog/post"
|
|
57
|
+
return normalizeQueryPathname(input);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Convert a canonical pathname into segment array form.
|
|
61
|
+
* Inverse of {@link segmentsToPathname}.
|
|
62
|
+
*
|
|
63
|
+
* pathnameToSegments("") // []
|
|
64
|
+
* pathnameToSegments("accueil") // ["accueil"]
|
|
65
|
+
* pathnameToSegments("blog/post") // ["blog", "post"]
|
|
66
|
+
*/
|
|
67
|
+
export function pathnameToSegments(pathname) {
|
|
68
|
+
if (pathname === '')
|
|
69
|
+
return [];
|
|
70
|
+
return pathname.split('/');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert a segment array into canonical pathname string form.
|
|
74
|
+
* Inverse of {@link pathnameToSegments}.
|
|
75
|
+
*
|
|
76
|
+
* segmentsToPathname([]) // ""
|
|
77
|
+
* segmentsToPathname(["accueil"]) // "accueil"
|
|
78
|
+
* segmentsToPathname(["blog", "post"]) // "blog/post"
|
|
79
|
+
*/
|
|
80
|
+
export function segmentsToPathname(segments) {
|
|
81
|
+
return segments.join('/');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Convert a canonical pathname (`'blog/post-1'` or `''` for homepage) to a
|
|
85
|
+
* browser-displayable path with leading slash. Inverse of {@link normalizePathname}.
|
|
86
|
+
*
|
|
87
|
+
* pathnameToHref('') → '/'
|
|
88
|
+
* pathnameToHref('accueil') → '/accueil'
|
|
89
|
+
* pathnameToHref('blog/post-1') → '/blog/post-1'
|
|
90
|
+
*
|
|
91
|
+
* Trusts canonical-form input (no leading slash, `''` for homepage). Producers
|
|
92
|
+
* are expected to call `normalizePathname` first if they hold non-canonical
|
|
93
|
+
* input. This keeps the helper minimal and the canonicalization rule single-sourced.
|
|
94
|
+
*
|
|
95
|
+
* Use cases: ADR-094 catch-all routing (companion utility consumed by customer
|
|
96
|
+
* renderers + the Studio iframe routing in ADR-095), future sitemap generation,
|
|
97
|
+
* AI Builder doc→URL pass, "share my draft" link generators.
|
|
98
|
+
*/
|
|
99
|
+
export function pathnameToHref(canonical) {
|
|
100
|
+
if (canonical === '')
|
|
101
|
+
return '/';
|
|
102
|
+
return `/${canonical}`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type PreviewFrameDependencyLike = {
|
|
2
|
+
readonly kind: 'document';
|
|
3
|
+
readonly documentId: string;
|
|
4
|
+
readonly storyType?: string;
|
|
5
|
+
readonly reason?: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'asset';
|
|
8
|
+
readonly assetId: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'list';
|
|
11
|
+
readonly storyType: string;
|
|
12
|
+
readonly where?: unknown;
|
|
13
|
+
readonly orderBy?: {
|
|
14
|
+
readonly field: string;
|
|
15
|
+
readonly direction: 'asc' | 'desc';
|
|
16
|
+
};
|
|
17
|
+
readonly limit?: number;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'route-lookup';
|
|
20
|
+
readonly pathname: string;
|
|
21
|
+
readonly storyTypes?: readonly string[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'get-by-slug';
|
|
24
|
+
readonly storyType: string;
|
|
25
|
+
readonly slug: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'get-by-pathname';
|
|
28
|
+
readonly storyType: string;
|
|
29
|
+
readonly pathname: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'batch-get';
|
|
32
|
+
readonly documentIds: readonly string[];
|
|
33
|
+
};
|
|
34
|
+
export declare function attachPreviewFrameDependencies<T>(target: T, dependencies: readonly PreviewFrameDependencyLike[]): T;
|
|
35
|
+
export declare function getAttachedPreviewFrameDependencies(value: unknown): readonly PreviewFrameDependencyLike[];
|
|
36
|
+
/** Build precise Preview dependencies from public `_assets` sidecars. */
|
|
37
|
+
export declare function previewAssetDependenciesFromValue(value: unknown): readonly Extract<PreviewFrameDependencyLike, {
|
|
38
|
+
readonly kind: 'asset';
|
|
39
|
+
}>[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getPreviewReadDependencies, setPreviewReadDependencies } from '@storystick/types';
|
|
2
|
+
export function attachPreviewFrameDependencies(target, dependencies) {
|
|
3
|
+
if (!isObjectLike(target) || dependencies.length === 0)
|
|
4
|
+
return target;
|
|
5
|
+
const next = dedupePreviewFrameDependencies([
|
|
6
|
+
...getAttachedPreviewFrameDependencies(target),
|
|
7
|
+
...dependencies,
|
|
8
|
+
]);
|
|
9
|
+
setPreviewReadDependencies(target, next);
|
|
10
|
+
return target;
|
|
11
|
+
}
|
|
12
|
+
export function getAttachedPreviewFrameDependencies(value) {
|
|
13
|
+
if (!isObjectLike(value))
|
|
14
|
+
return [];
|
|
15
|
+
return getPreviewReadDependencies(value).filter(isPreviewDependencyLike);
|
|
16
|
+
}
|
|
17
|
+
/** Build precise Preview dependencies from public `_assets` sidecars. */
|
|
18
|
+
export function previewAssetDependenciesFromValue(value) {
|
|
19
|
+
const assetIds = new Set();
|
|
20
|
+
const seen = new WeakSet();
|
|
21
|
+
const visit = (current) => {
|
|
22
|
+
if (!isObjectLike(current) || seen.has(current))
|
|
23
|
+
return;
|
|
24
|
+
seen.add(current);
|
|
25
|
+
if (Array.isArray(current)) {
|
|
26
|
+
for (const item of current)
|
|
27
|
+
visit(item);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const record = current;
|
|
31
|
+
const sidecar = isPlainRecord(record._assets) ? record._assets : undefined;
|
|
32
|
+
for (const key of ['byId', 'unresolved']) {
|
|
33
|
+
const entries = sidecar && isPlainRecord(sidecar[key])
|
|
34
|
+
? sidecar[key]
|
|
35
|
+
: undefined;
|
|
36
|
+
if (entries) {
|
|
37
|
+
for (const assetId of Object.keys(entries)) {
|
|
38
|
+
if (assetId.length > 0)
|
|
39
|
+
assetIds.add(assetId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const child of Object.values(record))
|
|
44
|
+
visit(child);
|
|
45
|
+
};
|
|
46
|
+
visit(value);
|
|
47
|
+
return [...assetIds]
|
|
48
|
+
.sort()
|
|
49
|
+
.map((assetId) => ({ kind: 'asset', assetId }));
|
|
50
|
+
}
|
|
51
|
+
function dedupePreviewFrameDependencies(dependencies) {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const dependency of dependencies) {
|
|
55
|
+
const key = stableStringify(dependency);
|
|
56
|
+
if (seen.has(key))
|
|
57
|
+
continue;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
out.push(dependency);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function isPreviewDependencyLike(value) {
|
|
64
|
+
return isPlainRecord(value) && typeof value.kind === 'string';
|
|
65
|
+
}
|
|
66
|
+
function stableStringify(value) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
69
|
+
}
|
|
70
|
+
if (isPlainRecord(value)) {
|
|
71
|
+
return `{${Object.keys(value)
|
|
72
|
+
.sort()
|
|
73
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
74
|
+
.join(',')}}`;
|
|
75
|
+
}
|
|
76
|
+
return JSON.stringify(value);
|
|
77
|
+
}
|
|
78
|
+
function isObjectLike(value) {
|
|
79
|
+
return (typeof value === 'object' || typeof value === 'function') &&
|
|
80
|
+
value !== null;
|
|
81
|
+
}
|
|
82
|
+
function isPlainRecord(value) {
|
|
83
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
84
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PreviewQueryReceiptId } from '@storystick/types/preview-query-targeting';
|
|
2
|
+
export declare function attachPreviewQueryReceipt<T>(result: T, receiptId: string | undefined): T;
|
|
3
|
+
export declare function getPreviewQueryReceiptId(result: unknown): PreviewQueryReceiptId | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const receiptByResult = new WeakMap();
|
|
2
|
+
export function attachPreviewQueryReceipt(result, receiptId) {
|
|
3
|
+
if (result !== null &&
|
|
4
|
+
(typeof result === 'object' || typeof result === 'function') &&
|
|
5
|
+
typeof receiptId === 'string' &&
|
|
6
|
+
/^pqr_[a-f0-9]{64}$/.test(receiptId)) {
|
|
7
|
+
receiptByResult.set(result, receiptId);
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
export function getPreviewQueryReceiptId(result) {
|
|
12
|
+
if (result === null ||
|
|
13
|
+
(typeof result !== 'object' && typeof result !== 'function'))
|
|
14
|
+
return null;
|
|
15
|
+
return receiptByResult.get(result) ?? null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PreviewProvenanceReadLifecycleInput, RequestContext } from './types.js';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export declare function runPreviewProvenanceReadLifecycle<T>(requestContext: RequestContext | undefined, input: PreviewProvenanceReadLifecycleInput<T>): Promise<T>;
|
|
4
|
+
/**
|
|
5
|
+
* Stable, non-secret JSON key used only by request-local adapter caches.
|
|
6
|
+
* Functions, undefined values, and AbortSignals are intentionally omitted.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare function createPreviewProvenanceReadCacheKey(kind: PreviewProvenanceReadLifecycleInput<unknown>['kind'], value: unknown): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { prepareContentValues } from './content-values.js';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export function runPreviewProvenanceReadLifecycle(requestContext, input) {
|
|
4
|
+
const lifecycle = requestContext?.unstable_previewProvenanceReadLifecycle;
|
|
5
|
+
return (lifecycle ? lifecycle(input) : input.executeRaw()).then(value => prepareContentValues(value, true, Boolean(requestContext?.previewToken)));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Stable, non-secret JSON key used only by request-local adapter caches.
|
|
9
|
+
* Functions, undefined values, and AbortSignals are intentionally omitted.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function createPreviewProvenanceReadCacheKey(kind, value) {
|
|
14
|
+
return JSON.stringify({ contentVersion: 1, kind, value: canonicalize(value) });
|
|
15
|
+
}
|
|
16
|
+
function canonicalize(value) {
|
|
17
|
+
if (value === null ||
|
|
18
|
+
typeof value === 'string' ||
|
|
19
|
+
typeof value === 'number' ||
|
|
20
|
+
typeof value === 'boolean') {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value.map(canonicalize);
|
|
25
|
+
if (typeof value !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
if (isAbortSignal(value))
|
|
28
|
+
return undefined;
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const key of Object.keys(value).sort()) {
|
|
31
|
+
const next = canonicalize(value[key]);
|
|
32
|
+
if (next !== undefined)
|
|
33
|
+
result[key] = next;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function isAbortSignal(value) {
|
|
38
|
+
return (typeof value.aborted === 'boolean' &&
|
|
39
|
+
typeof value.addEventListener === 'function');
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PreviewReadContext } from '@storystick/types/preview-query-targeting';
|
|
2
|
+
export interface PreviewReadObservation {
|
|
3
|
+
readonly readContext?: PreviewReadContext;
|
|
4
|
+
readonly receiptId?: string;
|
|
5
|
+
readonly documentIds: readonly string[];
|
|
6
|
+
readonly storyTypes: readonly string[];
|
|
7
|
+
readonly codeVersion: string;
|
|
8
|
+
readonly observationId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Clock sampled before the gateway content read. Multiple reads in one RSC
|
|
11
|
+
* render compose with MIN, never max: equal clocks are conservatively stale.
|
|
12
|
+
*/
|
|
13
|
+
readonly renderWatermark: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function attachPreviewReadObservation<T>(root: T, observation: PreviewReadObservation): T;
|
|
16
|
+
export declare function getPreviewReadObservation(value: unknown): PreviewReadObservation | null;
|
|
17
|
+
export declare function collectPreviewReadObservations(...roots: readonly unknown[]): readonly PreviewReadObservation[];
|
|
18
|
+
export declare function conservativePreviewRenderWatermark(observations: readonly Pick<PreviewReadObservation, 'renderWatermark'>[]): number | undefined;
|