@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.
Files changed (75) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +22 -0
  3. package/README.md +180 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/_fetchers.d.ts +51 -0
  6. package/dist/_fetchers.js +144 -0
  7. package/dist/_stateMachine.d.ts +102 -0
  8. package/dist/_stateMachine.js +168 -0
  9. package/dist/_wireAuth.d.ts +48 -0
  10. package/dist/_wireAuth.js +52 -0
  11. package/dist/adapter.d.ts +14 -0
  12. package/dist/adapter.js +10 -0
  13. package/dist/content-values.d.ts +11 -0
  14. package/dist/content-values.js +401 -0
  15. package/dist/errors.d.ts +65 -0
  16. package/dist/errors.js +45 -0
  17. package/dist/hydrate-slice.d.ts +2 -0
  18. package/dist/hydrate-slice.js +1 -0
  19. package/dist/index.d.ts +23 -0
  20. package/dist/index.js +9 -0
  21. package/dist/infra-resolver.d.ts +94 -0
  22. package/dist/infra-resolver.js +130 -0
  23. package/dist/internal.d.ts +33 -0
  24. package/dist/internal.js +20 -0
  25. package/dist/locale-routing.d.ts +5 -0
  26. package/dist/locale-routing.js +33 -0
  27. package/dist/normalize-pathname.d.ts +79 -0
  28. package/dist/normalize-pathname.js +103 -0
  29. package/dist/preview-frame-dependencies.d.ts +39 -0
  30. package/dist/preview-frame-dependencies.js +84 -0
  31. package/dist/preview-query-receipt.d.ts +3 -0
  32. package/dist/preview-query-receipt.js +16 -0
  33. package/dist/preview-read-lifecycle.d.ts +10 -0
  34. package/dist/preview-read-lifecycle.js +40 -0
  35. package/dist/preview-read-observation.d.ts +18 -0
  36. package/dist/preview-read-observation.js +81 -0
  37. package/dist/preview-scope.d.ts +3 -0
  38. package/dist/preview-scope.js +22 -0
  39. package/dist/preview-target.d.ts +73 -0
  40. package/dist/preview-target.js +86 -0
  41. package/dist/project-locales.d.ts +4 -0
  42. package/dist/project-locales.js +12 -0
  43. package/dist/public-delivery.d.ts +36 -0
  44. package/dist/public-delivery.js +55 -0
  45. package/dist/published-routes.d.ts +3 -0
  46. package/dist/published-routes.js +100 -0
  47. package/dist/query-builder.d.ts +225 -0
  48. package/dist/query-builder.js +294 -0
  49. package/dist/query-client.d.ts +54 -0
  50. package/dist/query-client.js +306 -0
  51. package/dist/query-compiler.d.ts +5 -0
  52. package/dist/query-compiler.js +12 -0
  53. package/dist/query-schema.d.ts +32 -0
  54. package/dist/query-schema.js +1 -0
  55. package/dist/redirect-search.d.ts +4 -0
  56. package/dist/redirect-search.js +14 -0
  57. package/dist/reference-resolver.d.ts +37 -0
  58. package/dist/reference-resolver.js +126 -0
  59. package/dist/resolve-route.d.ts +27 -0
  60. package/dist/resolve-route.js +145 -0
  61. package/dist/resolve.d.ts +43 -0
  62. package/dist/resolve.js +160 -0
  63. package/dist/schemas.d.ts +71 -0
  64. package/dist/schemas.js +75 -0
  65. package/dist/seo.d.ts +52 -0
  66. package/dist/seo.js +93 -0
  67. package/dist/signed-webhook.d.ts +13 -0
  68. package/dist/signed-webhook.js +54 -0
  69. package/dist/transport.d.ts +29 -0
  70. package/dist/transport.js +584 -0
  71. package/dist/typed-route.d.ts +22 -0
  72. package/dist/typed-route.js +1 -0
  73. package/dist/types.d.ts +147 -0
  74. package/dist/types.js +1 -0
  75. package/package.json +70 -3
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Storystick SDK token state machine (ADR-045 §13).
3
+ *
4
+ * Pure state, zero Convex dep. Given a pluggable `fetcher` that returns
5
+ * `{ token, expiresAt }` from any issuance endpoint, the machine:
6
+ *
7
+ * 1. **Opportunistic JIT refresh** (§13.1) — on every `ensureToken()` call,
8
+ * if the cached token has ≤ `margin` seconds left, refresh BEFORE returning.
9
+ * Jitter on the margin per instance so a fleet cold-start doesn't thunder
10
+ * the gateway all at once.
11
+ * 2. **Single-flight coalescing** (§13.2) — N concurrent calls during a
12
+ * refresh share the same `pendingRefresh` promise, one HTTP call out.
13
+ * 3. **DEAD state** (§13.3) — hard-auth errors (INVALID_API_KEY, revoked,
14
+ * expired, not-a-member) lock the machine; all subsequent calls throw
15
+ * immediately. Exit only via explicit `reset()`.
16
+ * 4. **Reactive 401 backstop** — exposed as `forceRefresh()` for the wire
17
+ * layer to call when a Convex query returns 401 in-flight.
18
+ *
19
+ * Not included here on purpose: the transient-error backoff (§13.7) lives
20
+ * in the wire layer (Convex client's built-in retry), not in this file.
21
+ */
22
+ // ─── Constants ──────────────────────────────────────────────────
23
+ const MARGIN_BASE_SECONDS = 60;
24
+ const MARGIN_JITTER_SECONDS = 10;
25
+ /**
26
+ * Hard-auth code list — post-batch-5 includes both namespaced (canonical)
27
+ * and bare (legacy-rollout-window) variants. The classifier prefers the
28
+ * canonical `category === 'auth' && !recoverable` signal when present (per
29
+ * Tension #5 lock B) ; this list is the fallback for SdkErrors that lack
30
+ * canonical metadata (e.g. legacy gateway versions, local non-wire errors).
31
+ */
32
+ const HARD_AUTH_ERRORS = [
33
+ 'storystick/INVALID_API_KEY',
34
+ 'storystick/API_KEY_REVOKED',
35
+ 'storystick/API_KEY_EXPIRED',
36
+ 'storystick/NOT_A_MEMBER',
37
+ 'storystick/ACCESS_DENIED',
38
+ // Legacy bare codes
39
+ 'INVALID_API_KEY',
40
+ 'API_KEY_REVOKED',
41
+ 'API_KEY_EXPIRED',
42
+ 'NOT_A_MEMBER',
43
+ 'ACCESS_DENIED',
44
+ ];
45
+ const REFRESHABLE_ERRORS = [
46
+ 'storystick/TOKEN_EXPIRED',
47
+ 'storystick/INVALID_TOKEN',
48
+ 'storystick/UNAUTHENTICATED',
49
+ // Legacy bare codes
50
+ 'TOKEN_EXPIRED',
51
+ 'INVALID_TOKEN',
52
+ 'UNAUTHENTICATED',
53
+ ];
54
+ /**
55
+ * Construct a fresh state machine. Per-instance state (cache, pendingRefresh,
56
+ * DEAD flag, margin) is captured in the closure returned here — one instance
57
+ * per Convex client. Module-level sharing between clients is intentionally
58
+ * NOT supported: different clients have different issuance endpoints,
59
+ * different keys, different tenant scopes.
60
+ */
61
+ export function createTokenStateMachine({ fetcher, marginSecondsOverride, }) {
62
+ // Per-instance jitter, set once at construction time (§13.1). Keeps the
63
+ // machine deterministic within a single instance's lifetime — the margin
64
+ // does not jitter on each call.
65
+ const marginSeconds = marginSecondsOverride ??
66
+ MARGIN_BASE_SECONDS + (Math.random() * 2 - 1) * MARGIN_JITTER_SECONDS;
67
+ let cache = null;
68
+ let pendingRefresh = null;
69
+ let dead = null;
70
+ async function fetchAndCache() {
71
+ try {
72
+ const result = await fetcher();
73
+ cache = result;
74
+ return result.token;
75
+ }
76
+ catch (err) {
77
+ if (isHardAuthError(err)) {
78
+ dead = err;
79
+ }
80
+ throw err;
81
+ }
82
+ }
83
+ async function forceRefresh() {
84
+ if (dead)
85
+ throw dead;
86
+ if (pendingRefresh)
87
+ return pendingRefresh;
88
+ pendingRefresh = fetchAndCache().finally(() => {
89
+ pendingRefresh = null;
90
+ });
91
+ return pendingRefresh;
92
+ }
93
+ async function ensureToken() {
94
+ if (dead)
95
+ throw dead;
96
+ const now = Date.now();
97
+ if (cache && cache.expiresAt > now + marginSeconds * 1000) {
98
+ return cache.token;
99
+ }
100
+ return forceRefresh();
101
+ }
102
+ function reset() {
103
+ cache = null;
104
+ pendingRefresh = null;
105
+ dead = null;
106
+ }
107
+ function isDead() {
108
+ return dead !== null;
109
+ }
110
+ return { ensureToken, forceRefresh, reset, isDead };
111
+ }
112
+ // ─── Error classification (exported for wire layer) ─────────────
113
+ //
114
+ // Post ADR-079 batch 5 (Tension #5 lock B — full routing refactor) :
115
+ // classifiers prefer the canonical envelope's `category` + `recoverable` +
116
+ // `retryable` fields when present. They fall back to hardcoded code lists
117
+ // (legacy bare + namespaced variants) when the SdkError lacks canonical
118
+ // metadata — the gateway-rollout window and local non-wire SdkErrors.
119
+ /**
120
+ * Refreshable error : the SDK should attempt a token refresh + retry.
121
+ * Canonical signal : `category: 'auth' && retryable: true` (e.g. TOKEN_EXPIRED,
122
+ * INVALID_TOKEN, UNAUTHENTICATED have `retryable: true` in the catalogue).
123
+ */
124
+ export function isRefreshableError(err) {
125
+ const meta = extractMetadata(err);
126
+ if (meta.category !== undefined && meta.retryable !== undefined) {
127
+ // Canonical fast path : auth-retryable means refreshable.
128
+ return meta.category === 'auth' && meta.retryable === true;
129
+ }
130
+ // Fallback : hardcoded refreshable code list.
131
+ return meta.code !== undefined && REFRESHABLE_ERRORS.includes(meta.code);
132
+ }
133
+ /**
134
+ * Hard auth error : the SDK should NOT retry, mark itself DEAD, surface to
135
+ * user. Canonical signal : `category: 'auth' && recoverable: false` (e.g.
136
+ * INVALID_API_KEY, ACCESS_DENIED).
137
+ */
138
+ export function isHardAuthError(err) {
139
+ const meta = extractMetadata(err);
140
+ if (meta.category !== undefined && meta.recoverable !== undefined) {
141
+ // Canonical fast path : auth + non-recoverable means hard.
142
+ return meta.category === 'auth' && meta.recoverable === false;
143
+ }
144
+ // Fallback : hardcoded hard-auth code list.
145
+ return meta.code !== undefined && HARD_AUTH_ERRORS.includes(meta.code);
146
+ }
147
+ function extractMetadata(err) {
148
+ if (err == null || typeof err !== 'object')
149
+ return {};
150
+ const obj = err;
151
+ const meta = {};
152
+ if (typeof obj.code === 'string')
153
+ meta.code = obj.code;
154
+ if (typeof obj.category === 'string')
155
+ meta.category = obj.category;
156
+ if (typeof obj.recoverable === 'boolean')
157
+ meta.recoverable = obj.recoverable;
158
+ if (typeof obj.retryable === 'boolean')
159
+ meta.retryable = obj.retryable;
160
+ if (meta.code === undefined && typeof obj.message === 'string') {
161
+ // ConvexError surfaces on `message` — parse the ADR error envelope.
162
+ // Post-batch-5 the namespaced form `storystick/<NAME>` matches too.
163
+ const match = obj.message.match(/"code"\s*:\s*"([\w/]+)"/);
164
+ if (match)
165
+ meta.code = match[1];
166
+ }
167
+ return meta;
168
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Wire the state machine into a Convex client's `setAuth` (ADR-045 §13).
3
+ *
4
+ * `ConvexReactClient.setAuth(fetchToken)` and `ConvexHttpClient.setAuth(...)`
5
+ * both accept an async function receiving `{ forceRefreshToken: boolean }`.
6
+ * This helper bridges that signature to our `TokenStateMachine`:
7
+ *
8
+ * - Regular call → `machine.ensureToken()` (opportunistic JIT).
9
+ * - `forceRefreshToken: true` signal from Convex (sent after a 401 on an
10
+ * in-flight query) → `machine.forceRefresh()` (reactive backstop, §13.6).
11
+ *
12
+ * This is the single seam between our SDK logic and Convex's client. Keep
13
+ * it thin: every new concern (logging, metrics, sentry hooks) lands here,
14
+ * not inside the state machine.
15
+ */
16
+ import { type TokenFetcher, type TokenStateMachine } from './_stateMachine.js';
17
+ /** Minimal shape our wire helper expects on a Convex client. */
18
+ export interface ConvexClientWithSetAuth {
19
+ setAuth(fetchToken: (args: {
20
+ forceRefreshToken: boolean;
21
+ }) => Promise<string | null>): void;
22
+ }
23
+ export interface WireAuthOptions {
24
+ fetcher: TokenFetcher;
25
+ /** Override the margin for tests. Default: 60s ± 10s jitter. */
26
+ marginSecondsOverride?: number;
27
+ }
28
+ export interface WireAuthResult {
29
+ /** The underlying state machine, exposed for `reset()` on sign-out. */
30
+ machine: TokenStateMachine;
31
+ /**
32
+ * Detach the `setAuth` wire by setting a no-op fetcher. Convex does not
33
+ * expose a first-class unsubscribe, so this is the conventional path:
34
+ * `setAuth(async () => null)` drops the machine.
35
+ */
36
+ detach: () => void;
37
+ }
38
+ /**
39
+ * Attach state-machine-backed auth to a Convex client.
40
+ *
41
+ * const { machine } = wireConvexAuth(client, { fetcher: apiKeyFetcher(...) })
42
+ * // on sign-out:
43
+ * machine.reset()
44
+ *
45
+ * The machine lives for the lifetime of the client. Build a new one per
46
+ * client — different clients have different issuance targets.
47
+ */
48
+ export declare function wireConvexAuth(client: ConvexClientWithSetAuth, { fetcher, marginSecondsOverride }: WireAuthOptions): WireAuthResult;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Wire the state machine into a Convex client's `setAuth` (ADR-045 §13).
3
+ *
4
+ * `ConvexReactClient.setAuth(fetchToken)` and `ConvexHttpClient.setAuth(...)`
5
+ * both accept an async function receiving `{ forceRefreshToken: boolean }`.
6
+ * This helper bridges that signature to our `TokenStateMachine`:
7
+ *
8
+ * - Regular call → `machine.ensureToken()` (opportunistic JIT).
9
+ * - `forceRefreshToken: true` signal from Convex (sent after a 401 on an
10
+ * in-flight query) → `machine.forceRefresh()` (reactive backstop, §13.6).
11
+ *
12
+ * This is the single seam between our SDK logic and Convex's client. Keep
13
+ * it thin: every new concern (logging, metrics, sentry hooks) lands here,
14
+ * not inside the state machine.
15
+ */
16
+ import { createTokenStateMachine, } from './_stateMachine.js';
17
+ /**
18
+ * Attach state-machine-backed auth to a Convex client.
19
+ *
20
+ * const { machine } = wireConvexAuth(client, { fetcher: apiKeyFetcher(...) })
21
+ * // on sign-out:
22
+ * machine.reset()
23
+ *
24
+ * The machine lives for the lifetime of the client. Build a new one per
25
+ * client — different clients have different issuance targets.
26
+ */
27
+ export function wireConvexAuth(client, { fetcher, marginSecondsOverride }) {
28
+ const machine = createTokenStateMachine({
29
+ fetcher,
30
+ marginSecondsOverride,
31
+ });
32
+ client.setAuth(async ({ forceRefreshToken }) => {
33
+ try {
34
+ return forceRefreshToken
35
+ ? await machine.forceRefresh()
36
+ : await machine.ensureToken();
37
+ }
38
+ catch {
39
+ // Convex treats `null` as "no auth available"; any exception thrown
40
+ // from `fetchToken` is logged by Convex as a warning. Returning null
41
+ // lets the component-side `requireTenant` surface `UNAUTHENTICATED`
42
+ // cleanly, which the app layer catches.
43
+ return null;
44
+ }
45
+ });
46
+ return {
47
+ machine,
48
+ detach() {
49
+ client.setAuth(async () => null);
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,14 @@
1
+ /** Framework-adapter infrastructure. Application code should use createClient. */
2
+ export { resolveProject } from './resolve.js';
3
+ export type { ProjectConfig } from './resolve.js';
4
+ export { resolveInfra, resolveInfraSync } from './infra-resolver.js';
5
+ export type { ResolveInfraOptions, ResolvedInfra } from './infra-resolver.js';
6
+ export { createHttpTransportV2 } from './transport.js';
7
+ export type { HttpTransportOptions } from './transport.js';
8
+ export { resolveStorystickPage } from './resolve-route.js';
9
+ export { fetchCurrentSchema, isSchemaFormatUnsupportedError, SCHEMA_FORMAT_UNSUPPORTED_CODE, } from './schemas.js';
10
+ export type { RawSchemaRow, SchemaFormatUnsupportedPayload, } from './schemas.js';
11
+ export { publishedLocaleRoutes } from './published-routes.js';
12
+ export { fetchProjectLocaleSettings } from './project-locales.js';
13
+ export { appendSearchParamsToRedirect } from './redirect-search.js';
14
+ export { readSignedWebhook, signWebhookBody } from './signed-webhook.js';
@@ -0,0 +1,10 @@
1
+ /** Framework-adapter infrastructure. Application code should use createClient. */
2
+ export { resolveProject } from './resolve.js';
3
+ export { resolveInfra, resolveInfraSync } from './infra-resolver.js';
4
+ export { createHttpTransportV2 } from './transport.js';
5
+ export { resolveStorystickPage } from './resolve-route.js';
6
+ export { fetchCurrentSchema, isSchemaFormatUnsupportedError, SCHEMA_FORMAT_UNSUPPORTED_CODE, } from './schemas.js';
7
+ export { publishedLocaleRoutes } from './published-routes.js';
8
+ export { fetchProjectLocaleSettings } from './project-locales.js';
9
+ export { appendSearchParamsToRedirect } from './redirect-search.js';
10
+ export { readSignedWebhook, signWebhookBody } from './signed-webhook.js';
@@ -0,0 +1,11 @@
1
+ export interface ContentSource {
2
+ document: unknown;
3
+ path: string;
4
+ }
5
+ export { readContentOrigin, type ContentOrigin } from '@storystick/types/internal/content-origin';
6
+ /** Internal: live projections apply to authored reads before effective hydration. */
7
+ export declare function rawContentValues<T>(value: T): T;
8
+ /** Prepare once per consumer, after the raw read cache and Preview coding. */
9
+ export declare function prepareContentValues<T>(value: T, requirePlan?: boolean, preview?: boolean): T;
10
+ /** Explicit raw-content escape hatch. Never performs I/O or reads app context. */
11
+ export declare function prepareContentSource<T>(value: T, source?: ContentSource): T;