@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,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';
|
package/dist/adapter.js
ADDED
|
@@ -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;
|