@elytracms/studio 0.0.65 → 0.0.66
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/dist/components/account/self-account-view-source.js +9 -2
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +28 -1
- package/dist/components/app-views/default-modules.js +47 -2
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/auth-context.js +32 -1
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.js +4 -2
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-dnd.js +4 -2
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/insert-context.js +15 -6
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +4 -2
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +2 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/asset-field.js +21 -5
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +22 -3
- package/dist/components/cms/bulk-actions.js +27 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cms-store.js +19 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +18 -19
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +23 -16
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +21 -0
- package/dist/components/cms/publish-clean.js +30 -0
- package/dist/components/cms/publish-clean.js.map +1 -0
- package/dist/components/cms/use-live-url.d.ts +24 -1
- package/dist/components/cms/use-live-url.js +26 -4
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +8 -6
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +15 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/lib/auth/auth-adapter.d.ts +22 -0
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
- package/dist/lib/auth/convex-auth-adapter.js +69 -0
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
- package/dist/lib/auth/local-auth-adapter.js +7 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +45 -3
- package/dist/lib/auth-errors.js +46 -5
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +5 -2
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +13 -0
- package/dist/lib/dnd/drop-rules.js +15 -0
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +3 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +2 -0
- package/dist/lib/i18n/messages.en.js +8 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -3
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +19 -6
- package/dist/lib/session-expiry.js +13 -1
- package/dist/lib/session-expiry.js.map +1 -1
- package/dist/lib/session-revalidation.d.ts +81 -0
- package/dist/lib/session-revalidation.js +94 -0
- package/dist/lib/session-revalidation.js.map +1 -0
- package/package.json +2 -2
|
@@ -31,7 +31,6 @@ export async function selectPersistenceAdapter() {
|
|
|
31
31
|
// sit under the same id the studio operates on. Otherwise authorization denies
|
|
32
32
|
// every read ("not a member of project 'aurora'") and content resolves empty.
|
|
33
33
|
const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID);
|
|
34
|
-
const adapter = await createSeededPersistenceAdapter(projectId);
|
|
35
34
|
// EC-361 (AD-11 parity with the Convex adapter above): when a config project
|
|
36
35
|
// declares collections, the playground serves the CONFIG-OWNED schema — not the
|
|
37
36
|
// engine's seeded sample schema — so every schema reader (the content-session
|
|
@@ -39,9 +38,13 @@ export async function selectPersistenceAdapter() {
|
|
|
39
38
|
// the same declarations connected mode serves. The resolved config already
|
|
40
39
|
// carries the EC-257 default asset collection. Without a config (engine
|
|
41
40
|
// fixtures/tests) the seeded sample schema stands, byte-identical to before.
|
|
41
|
+
// EC-424: passing `collections` straight into the seed (not a follow-up
|
|
42
|
+
// `saveSchema`) also reconciles the seeded DOCUMENTS against them — a field a
|
|
43
|
+
// config declares localized but the shared fixture stores plain (e.g. this
|
|
44
|
+
// repo's demo `post.slug`, EC-315) gets promoted into `localized`, not just
|
|
45
|
+
// the schema — see `reconcileSeedDocumentLocalization` in `@elytracms/core/persistence`.
|
|
42
46
|
const collections = resolveDeploymentCollections();
|
|
43
|
-
|
|
44
|
-
await adapter.cms.saveSchema(projectId, collections);
|
|
47
|
+
const adapter = await createSeededPersistenceAdapter(projectId, collections.length > 0 ? collections : undefined);
|
|
45
48
|
return adapter;
|
|
46
49
|
}
|
|
47
50
|
throw new Error(config.detail);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/persistence/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAA;AAE7F,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,wBAAwB,CACjC,eAAe,CAAC,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EACjD,0BAA0B,CAAC,kBAAkB,CAAC;QAC9C,sEAAsE;QACtE,0EAA0E;QAC1E,sCAAsC;QACtC,4BAA4B,EAAE,CAC/B,CAAA;IACH,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACjC,6EAA6E;QAC7E,0DAA0D;QAC1D,0EAA0E;QAC1E,+EAA+E;QAC/E,+EAA+E;QAC/E,8EAA8E;QAC9E,MAAM,SAAS,GAAG,0BAA0B,CAAC,eAAe,CAAC,CAAA;QAC7D,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/persistence/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAA;AAE7F,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,wBAAwB,CACjC,eAAe,CAAC,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EACjD,0BAA0B,CAAC,kBAAkB,CAAC;QAC9C,sEAAsE;QACtE,0EAA0E;QAC1E,sCAAsC;QACtC,4BAA4B,EAAE,CAC/B,CAAA;IACH,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACjC,6EAA6E;QAC7E,0DAA0D;QAC1D,0EAA0E;QAC1E,+EAA+E;QAC/E,+EAA+E;QAC/E,8EAA8E;QAC9E,MAAM,SAAS,GAAG,0BAA0B,CAAC,eAAe,CAAC,CAAA;QAC7D,6EAA6E;QAC7E,gFAAgF;QAChF,8EAA8E;QAC9E,+EAA+E;QAC/E,2EAA2E;QAC3E,wEAAwE;QACxE,6EAA6E;QAC7E,wEAAwE;QACxE,8EAA8E;QAC9E,2EAA2E;QAC3E,4EAA4E;QAC5E,yFAAyF;QACzF,MAAM,WAAW,GAAG,4BAA4B,EAAE,CAAA;QAClD,MAAM,OAAO,GAAG,MAAM,8BAA8B,CAClD,SAAS,EACT,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CACjD,CAAA;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACrE,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,YAAY,CAAA;IACrD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACnE,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC;AAED,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,8BAA8B,EAC9B,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,GACV,MAAM,gBAAgB,CAAA;AAQvB,kFAAkF;AAClF,gFAAgF;AAChF,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA","sourcesContent":["import { SEED_PROJECT_ID, createSeededPersistenceAdapter } from '@elytracms/core/persistence'\nimport type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport {\n resolveDeploymentCollections,\n resolveDeploymentProjectId,\n resolveDeploymentUrl,\n} from '../workspace'\nimport { getConvexClient } from './convex-client'\nimport { ConvexPersistenceAdapter } from './convex-adapter'\nimport { resolveBackendConfig } from './backend-mode'\nimport type { StudioBackendMode } from './backend-mode'\n\n/**\n * Select the persistence backend (EC-153, vision AD-1). **Convex is the\n * default**: a configured `VITE_CONVEX_URL` is required unless the explicit\n * playground flag (`VITE_ELYTRA_MODE=playground`) opts into the deterministic,\n * seeded in-memory adapter. There is **no automatic in-memory fallback** —\n * missing configuration is a hard, descriptive error (the `DeploymentSetupGate`\n * renders it as a boot-error screen before any adapter is requested).\n *\n * EC-150: the Convex path uses the *shared* client so the auth adapter's\n * session JWT (set via `client.setAuth`) authenticates every persistence call.\n */\nexport async function selectPersistenceAdapter(): Promise<PersistenceAdapter> {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n return new ConvexPersistenceAdapter(\n getConvexClient(resolveDeploymentUrl(config.url)),\n resolveDeploymentProjectId('aurora-marketing'),\n // CMS collections are config-owned (EC-183, AD-11): inject the active\n // project's collections so the adapter serves the schema from config (the\n // backend has no `cmsSchemas` table).\n resolveDeploymentCollections(),\n )\n }\n if (config.kind === 'playground') {\n // EC-207: routes/redirects are config-sourced, never seeded to the backend —\n // the playground adapter no longer seeds route documents.\n // EC-310: key the seed under the ACTIVE project id (the config slug, e.g.\n // `aurora`) — not the engine's default `prj_aurora` — so content + memberships\n // sit under the same id the studio operates on. Otherwise authorization denies\n // every read (\"not a member of project 'aurora'\") and content resolves empty.\n const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID)\n // EC-361 (AD-11 parity with the Convex adapter above): when a config project\n // declares collections, the playground serves the CONFIG-OWNED schema — not the\n // engine's seeded sample schema — so every schema reader (the content-session\n // hydration, the persister baseline, query validation's `filterable` set) sees\n // the same declarations connected mode serves. The resolved config already\n // carries the EC-257 default asset collection. Without a config (engine\n // fixtures/tests) the seeded sample schema stands, byte-identical to before.\n // EC-424: passing `collections` straight into the seed (not a follow-up\n // `saveSchema`) also reconciles the seeded DOCUMENTS against them — a field a\n // config declares localized but the shared fixture stores plain (e.g. this\n // repo's demo `post.slug`, EC-315) gets promoted into `localized`, not just\n // the schema — see `reconcileSeedDocumentLocalization` in `@elytracms/core/persistence`.\n const collections = resolveDeploymentCollections()\n const adapter = await createSeededPersistenceAdapter(\n projectId,\n collections.length > 0 ? collections : undefined,\n )\n return adapter\n }\n throw new Error(config.detail)\n}\n\n/**\n * A stable key identifying the active backend (EC-179): the resolved deployment\n * URL for the active project under Convex, or `playground`. The operations\n * bootstrap caches one bundle per key, so switching to a project on a different\n * deployment transparently selects (or builds) that backend's operations client.\n */\nexport function activeDeploymentKey(): string {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') return resolveDeploymentUrl(config.url)\n if (config.kind === 'playground') return 'playground'\n throw new Error(config.detail)\n}\n\n/**\n * The active backend mode. Throws the same descriptive error as\n * {@link selectPersistenceAdapter} when the studio is misconfigured — UI that\n * only needs \"is this the playground?\" should use `isPlaygroundMode()`.\n */\nexport function persistenceMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\nexport { ConvexPersistenceAdapter } from './convex-adapter'\nexport {\n CONVEX_URL_ENV_VAR,\n DEV_LOGIN_ENV_VAR,\n MODE_ENV_VAR,\n PLAYGROUND_MODE,\n STUDIO_ENV_KEYS,\n detectStudioEnvAnomaly,\n isDevAutoLoginActive,\n isDevRuntime,\n isPlaygroundMode,\n provideStudioEnv,\n resetProvidedStudioEnvForTests,\n resolveBackendConfig,\n resolveDevAutoLogin,\n studioEnv,\n} from './backend-mode'\nexport type {\n DevAutoLoginCredentials,\n EnvRecord,\n StudioBackendConfig,\n StudioBackendMode,\n StudioEnvAnomaly,\n} from './backend-mode'\n// EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a\n// future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).\nexport { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness'\nexport type { StudioReadinessInputs } from './deployment-readiness'\n"]}
|
|
@@ -19,12 +19,15 @@ import type { OperationError, OperationEvent, OperationEventBus } from './operat
|
|
|
19
19
|
* state stay alive. This module is deliberately framework-free (a plain
|
|
20
20
|
* listener registry + pure predicates) so the state machine is unit-testable.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* Since EC-415 detection is ALSO proactive: the adapters carry a tri-state
|
|
23
|
+
* `probeSession()` (valid / invalid / unreachable), and the scheduler core in
|
|
24
|
+
* `lib/session-revalidation.ts` — wired to window focus + a modest visible-tab
|
|
25
|
+
* interval in `components/auth-context.tsx` — funnels a probe's `invalid`
|
|
26
|
+
* answer into this SAME chokepoint via {@link raiseSessionExpiry}. One
|
|
27
|
+
* listener registry, one auth-store guard, one overlay — no second raise
|
|
28
|
+
* path. `unreachable` stays silent by contract (the probe never clears the
|
|
29
|
+
* token; the operations layer already surfaces backend unavailability,
|
|
30
|
+
* EC-362/363).
|
|
28
31
|
*/
|
|
29
32
|
type SessionExpiryListener = () => void;
|
|
30
33
|
/**
|
|
@@ -39,6 +42,16 @@ export declare function onSessionExpiry(listener: SessionExpiryListener): () =>
|
|
|
39
42
|
* errors are ignored — callers can funnel every failure through unconditionally.
|
|
40
43
|
*/
|
|
41
44
|
export declare function reportSessionExpiredError(error: OperationError): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Raise session expiry directly (EC-415): the proactive probe path — the auth
|
|
47
|
+
* adapter's `probeSession()` answered `invalid`, so there is no failed
|
|
48
|
+
* operation whose error could carry the predicate through
|
|
49
|
+
* {@link reportSessionExpiredError}. Notifies the SAME listener registry, so
|
|
50
|
+
* the auth store's guarded subscription (`shouldBlockOnSessionExpiry`:
|
|
51
|
+
* signed-in only, idempotent while blocked) and the one blocking overlay
|
|
52
|
+
* (EC-403) serve both detection paths — never a second raise mechanism.
|
|
53
|
+
*/
|
|
54
|
+
export declare function raiseSessionExpiry(): void;
|
|
42
55
|
/**
|
|
43
56
|
* Is this operation lifecycle event a session-expiry failure? Only `failed`
|
|
44
57
|
* events can carry the backend's unauthenticated transport blob (`rejected` is
|
|
@@ -30,9 +30,21 @@ export function reportSessionExpiredError(error) {
|
|
|
30
30
|
if (reportedErrors.has(error))
|
|
31
31
|
return false;
|
|
32
32
|
reportedErrors.add(error);
|
|
33
|
+
raiseSessionExpiry();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Raise session expiry directly (EC-415): the proactive probe path — the auth
|
|
38
|
+
* adapter's `probeSession()` answered `invalid`, so there is no failed
|
|
39
|
+
* operation whose error could carry the predicate through
|
|
40
|
+
* {@link reportSessionExpiredError}. Notifies the SAME listener registry, so
|
|
41
|
+
* the auth store's guarded subscription (`shouldBlockOnSessionExpiry`:
|
|
42
|
+
* signed-in only, idempotent while blocked) and the one blocking overlay
|
|
43
|
+
* (EC-403) serve both detection paths — never a second raise mechanism.
|
|
44
|
+
*/
|
|
45
|
+
export function raiseSessionExpiry() {
|
|
33
46
|
for (const listener of [...listeners])
|
|
34
47
|
listener();
|
|
35
|
-
return true;
|
|
36
48
|
}
|
|
37
49
|
/**
|
|
38
50
|
* Is this operation lifecycle event a session-expiry failure? Only `failed`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-expiry.js","sourceRoot":"","sources":["../../src/lib/session-expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"session-expiry.js","sourceRoot":"","sources":["../../src/lib/session-expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAoCrD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAA;AAElD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAA+B;IAC7D,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,IAAI,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;AAElD;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqB;IAC7D,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC/C,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACzB,kBAAkB,EAAE,CAAA;IACpB,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB;IAChC,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;QAAE,QAAQ,EAAE,CAAA;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAqB;IACxD,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAyB;IACpE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC3D,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAG1C;IACC,OAAO,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,KAAK,CAAC,cAAc,CAAA;AAC9D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,0BAA0B;IACxC,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;IAC9C,SAAS,CAAC,KAAK,EAAE,CAAA;AACnB,CAAC","sourcesContent":["import { isSessionExpiredError } from './auth-errors'\nimport type { AuthStatus } from './auth'\nimport type { OperationError, OperationEvent, OperationEventBus } from './operations'\n\n/**\n * App-level session-expiry chokepoint (EC-403).\n *\n * Detection is reactive: an expired Convex session surfaces as a failed\n * operation whose error matches {@link isSessionExpiredError}. Every studio\n * command/query already flows through the operations pipeline, and every\n * failure is emitted on the shared {@link OperationEventBus} — so ONE\n * subscriber on that bus ({@link attachSessionExpiryReporting}, attached in\n * `createStudioOperations`) sees every expiry, no matter which surface issued\n * the operation. Surfaces that hold a structured `OperationError` directly\n * (the shared error banner) report through the same funnel\n * ({@link reportSessionExpiredError}).\n *\n * The consumer is the auth store (`components/auth-context.tsx`): it raises\n * one app-wide `sessionExpired` state which mounts the blocking re-auth\n * overlay — in place, no navigation, the React tree and all unsaved editor\n * state stay alive. This module is deliberately framework-free (a plain\n * listener registry + pure predicates) so the state machine is unit-testable.\n *\n * Since EC-415 detection is ALSO proactive: the adapters carry a tri-state\n * `probeSession()` (valid / invalid / unreachable), and the scheduler core in\n * `lib/session-revalidation.ts` — wired to window focus + a modest visible-tab\n * interval in `components/auth-context.tsx` — funnels a probe's `invalid`\n * answer into this SAME chokepoint via {@link raiseSessionExpiry}. One\n * listener registry, one auth-store guard, one overlay — no second raise\n * path. `unreachable` stays silent by contract (the probe never clears the\n * token; the operations layer already surfaces backend unavailability,\n * EC-362/363).\n */\n\ntype SessionExpiryListener = () => void\n\nconst listeners = new Set<SessionExpiryListener>()\n\n/**\n * Subscribe to session-expiry reports (the auth store does, once). Returns the\n * unsubscribe function.\n */\nexport function onSessionExpiry(listener: SessionExpiryListener): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Each expired-session error object is reported at most ONCE. The bus produces\n * a fresh error object per failure, so genuine new failures always report; but\n * a surface can keep rendering a *stale* error long after re-auth (a\n * `persistError` only clears on the next successful save), and remounting that\n * surface must not re-raise the overlay against a session that is valid again.\n */\nlet reportedErrors = new WeakSet<OperationError>()\n\n/**\n * Report a structured operation failure as a *possible* session expiry: runs\n * the {@link isSessionExpiredError} predicate, dedupes per error instance, and\n * notifies listeners. Returns whether listeners were notified. Non-expiry\n * errors are ignored — callers can funnel every failure through unconditionally.\n */\nexport function reportSessionExpiredError(error: OperationError): boolean {\n if (!isSessionExpiredError(error)) return false\n if (reportedErrors.has(error)) return false\n reportedErrors.add(error)\n raiseSessionExpiry()\n return true\n}\n\n/**\n * Raise session expiry directly (EC-415): the proactive probe path — the auth\n * adapter's `probeSession()` answered `invalid`, so there is no failed\n * operation whose error could carry the predicate through\n * {@link reportSessionExpiredError}. Notifies the SAME listener registry, so\n * the auth store's guarded subscription (`shouldBlockOnSessionExpiry`:\n * signed-in only, idempotent while blocked) and the one blocking overlay\n * (EC-403) serve both detection paths — never a second raise mechanism.\n */\nexport function raiseSessionExpiry(): void {\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Is this operation lifecycle event a session-expiry failure? Only `failed`\n * events can carry the backend's unauthenticated transport blob (`rejected` is\n * input validation, `denied` is the role hook — both structured, never expiry).\n */\nexport function isSessionExpiryEvent(event: OperationEvent): boolean {\n return event.status === 'failed' && event.error !== undefined && isSessionExpiredError(event.error)\n}\n\n/**\n * The one reactive detection seam (EC-403): subscribe to an operations event\n * bus and report every session-expiry failure. Attached per operations bundle\n * in `createStudioOperations`, so commands, queries, and changeset operations\n * are all covered. Returns the unsubscribe function.\n */\nexport function attachSessionExpiryReporting(events: OperationEventBus): () => void {\n return events.subscribe((event) => {\n if (event.status === 'failed' && event.error !== undefined) {\n reportSessionExpiredError(event.error)\n }\n })\n}\n\n/**\n * Pure raise-guard for the auth store's `sessionExpired` state: an expiry\n * report blocks the studio only when a live session exists (a failure surfacing\n * while signed out or still restoring must not overlay the sign-in screen),\n * and raising is idempotent while already blocked.\n */\nexport function shouldBlockOnSessionExpiry(input: {\n status: AuthStatus\n alreadyBlocked: boolean\n}): boolean {\n return input.status === 'signed-in' && !input.alreadyBlocked\n}\n\n/** Test seam: forget reported errors and listeners between test cases. */\nexport function resetSessionExpiryForTests(): void {\n reportedErrors = new WeakSet<OperationError>()\n listeners.clear()\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { SessionProbeResult } from './auth/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Proactive session re-validation scheduler (EC-415) — the pure, framework-free
|
|
4
|
+
* core behind the window-focus/interval probes wired up in
|
|
5
|
+
* `components/auth-context.tsx`.
|
|
6
|
+
*
|
|
7
|
+
* The adapters' tri-state `probeSession()` makes proactive checking SAFE
|
|
8
|
+
* (`unreachable` never clears the stored token — see `lib/auth/auth-adapter.ts`);
|
|
9
|
+
* this core makes it CHEAP and QUIET:
|
|
10
|
+
*
|
|
11
|
+
* - **Min gap:** at most one probe per {@link SESSION_PROBE_MIN_GAP_MS}
|
|
12
|
+
* (30 s), no matter how many focus/visibility/interval triggers fire — alt-
|
|
13
|
+
* tabbing back and forth never hammers the backend.
|
|
14
|
+
* - **In-flight dedupe:** never two concurrent probes.
|
|
15
|
+
* - **Gating:** probes run only over a live session (`signedIn`), never while
|
|
16
|
+
* the re-auth overlay is already up (`blocked` — nothing left to detect),
|
|
17
|
+
* and only while the tab is visible (`visible` — a background tab's session
|
|
18
|
+
* dying is caught by the focus probe the moment the editor returns).
|
|
19
|
+
* - **Routing:** `invalid` → {@link SessionRevalidatorOptions.onInvalid} (the
|
|
20
|
+
* EC-403 chokepoint, `raiseSessionExpiry`); `valid` and `unreachable` are
|
|
21
|
+
* silent. A probe that THROWS (contract violation) is treated as
|
|
22
|
+
* `unreachable` — a scheduler must never turn a transport surprise into a
|
|
23
|
+
* sign-out.
|
|
24
|
+
*
|
|
25
|
+
* Trigger cadence (the React wiring): window `focus` + document
|
|
26
|
+
* `visibilitychange` fire a trigger immediately (subject to the min gap), and
|
|
27
|
+
* a {@link SESSION_PROBE_INTERVAL_MS} (5 min) interval covers the
|
|
28
|
+
* tab-left-open-and-visible case. Proactive detection is a UX refinement on
|
|
29
|
+
* top of the reactive EC-403 chokepoint (any operation against a dead session
|
|
30
|
+
* still raises within one call), so the cadence is deliberately modest.
|
|
31
|
+
*/
|
|
32
|
+
/** At most one probe per this many milliseconds, across ALL trigger sources. */
|
|
33
|
+
export declare const SESSION_PROBE_MIN_GAP_MS = 30000;
|
|
34
|
+
/** Background probe cadence while the tab stays visible (the React interval). */
|
|
35
|
+
export declare const SESSION_PROBE_INTERVAL_MS: number;
|
|
36
|
+
/** The live studio state a probe is gated on (read fresh per trigger). */
|
|
37
|
+
export interface SessionRevalidatorGate {
|
|
38
|
+
/** A session exists to re-validate (`status === 'signed-in'`). */
|
|
39
|
+
readonly signedIn: boolean;
|
|
40
|
+
/** The re-auth overlay is already up — nothing left to detect. */
|
|
41
|
+
readonly blocked: boolean;
|
|
42
|
+
/** The tab is visible; hidden tabs wait for the return-focus probe. */
|
|
43
|
+
readonly visible: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pure per-trigger decision: should this trigger actually probe? Exported
|
|
47
|
+
* separately so the gating matrix is unit-pinnable without a scheduler.
|
|
48
|
+
*/
|
|
49
|
+
export declare function shouldProbeSession(input: {
|
|
50
|
+
gate: SessionRevalidatorGate;
|
|
51
|
+
probeInFlight: boolean;
|
|
52
|
+
lastProbeAt: number | null;
|
|
53
|
+
now: number;
|
|
54
|
+
minGapMs: number;
|
|
55
|
+
}): boolean;
|
|
56
|
+
export interface SessionRevalidatorOptions {
|
|
57
|
+
/** The adapter probe (`auth.probeSession()`). */
|
|
58
|
+
probe: () => Promise<SessionProbeResult>;
|
|
59
|
+
/** Funnel for an `invalid` answer — the EC-403 raise (`raiseSessionExpiry`). */
|
|
60
|
+
onInvalid: () => void;
|
|
61
|
+
/** Read the CURRENT gate state (refs, not a snapshot) at trigger time. */
|
|
62
|
+
readGate: () => SessionRevalidatorGate;
|
|
63
|
+
/** Override {@link SESSION_PROBE_MIN_GAP_MS} (tests). */
|
|
64
|
+
minGapMs?: number;
|
|
65
|
+
/** Clock override (tests). */
|
|
66
|
+
now?: () => number;
|
|
67
|
+
}
|
|
68
|
+
export interface SessionRevalidator {
|
|
69
|
+
/**
|
|
70
|
+
* Fire from any trigger source (focus, visibilitychange, interval tick).
|
|
71
|
+
* Resolves once the trigger is handled — immediately when gated away,
|
|
72
|
+
* after the probe settles when one runs.
|
|
73
|
+
*/
|
|
74
|
+
trigger(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create the scheduler. One instance per auth adapter (the React effect's
|
|
78
|
+
* lifetime); all internal state — last probe time, in-flight flag — lives
|
|
79
|
+
* here, keeping the React wiring a thin listener layer.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createSessionRevalidator(options: SessionRevalidatorOptions): SessionRevalidator;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive session re-validation scheduler (EC-415) — the pure, framework-free
|
|
3
|
+
* core behind the window-focus/interval probes wired up in
|
|
4
|
+
* `components/auth-context.tsx`.
|
|
5
|
+
*
|
|
6
|
+
* The adapters' tri-state `probeSession()` makes proactive checking SAFE
|
|
7
|
+
* (`unreachable` never clears the stored token — see `lib/auth/auth-adapter.ts`);
|
|
8
|
+
* this core makes it CHEAP and QUIET:
|
|
9
|
+
*
|
|
10
|
+
* - **Min gap:** at most one probe per {@link SESSION_PROBE_MIN_GAP_MS}
|
|
11
|
+
* (30 s), no matter how many focus/visibility/interval triggers fire — alt-
|
|
12
|
+
* tabbing back and forth never hammers the backend.
|
|
13
|
+
* - **In-flight dedupe:** never two concurrent probes.
|
|
14
|
+
* - **Gating:** probes run only over a live session (`signedIn`), never while
|
|
15
|
+
* the re-auth overlay is already up (`blocked` — nothing left to detect),
|
|
16
|
+
* and only while the tab is visible (`visible` — a background tab's session
|
|
17
|
+
* dying is caught by the focus probe the moment the editor returns).
|
|
18
|
+
* - **Routing:** `invalid` → {@link SessionRevalidatorOptions.onInvalid} (the
|
|
19
|
+
* EC-403 chokepoint, `raiseSessionExpiry`); `valid` and `unreachable` are
|
|
20
|
+
* silent. A probe that THROWS (contract violation) is treated as
|
|
21
|
+
* `unreachable` — a scheduler must never turn a transport surprise into a
|
|
22
|
+
* sign-out.
|
|
23
|
+
*
|
|
24
|
+
* Trigger cadence (the React wiring): window `focus` + document
|
|
25
|
+
* `visibilitychange` fire a trigger immediately (subject to the min gap), and
|
|
26
|
+
* a {@link SESSION_PROBE_INTERVAL_MS} (5 min) interval covers the
|
|
27
|
+
* tab-left-open-and-visible case. Proactive detection is a UX refinement on
|
|
28
|
+
* top of the reactive EC-403 chokepoint (any operation against a dead session
|
|
29
|
+
* still raises within one call), so the cadence is deliberately modest.
|
|
30
|
+
*/
|
|
31
|
+
/** At most one probe per this many milliseconds, across ALL trigger sources. */
|
|
32
|
+
export const SESSION_PROBE_MIN_GAP_MS = 30_000;
|
|
33
|
+
/** Background probe cadence while the tab stays visible (the React interval). */
|
|
34
|
+
export const SESSION_PROBE_INTERVAL_MS = 5 * 60_000;
|
|
35
|
+
/**
|
|
36
|
+
* Pure per-trigger decision: should this trigger actually probe? Exported
|
|
37
|
+
* separately so the gating matrix is unit-pinnable without a scheduler.
|
|
38
|
+
*/
|
|
39
|
+
export function shouldProbeSession(input) {
|
|
40
|
+
const { gate, probeInFlight, lastProbeAt, now, minGapMs } = input;
|
|
41
|
+
if (!gate.signedIn || gate.blocked || !gate.visible)
|
|
42
|
+
return false;
|
|
43
|
+
if (probeInFlight)
|
|
44
|
+
return false;
|
|
45
|
+
if (lastProbeAt !== null && now - lastProbeAt < minGapMs)
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create the scheduler. One instance per auth adapter (the React effect's
|
|
51
|
+
* lifetime); all internal state — last probe time, in-flight flag — lives
|
|
52
|
+
* here, keeping the React wiring a thin listener layer.
|
|
53
|
+
*/
|
|
54
|
+
export function createSessionRevalidator(options) {
|
|
55
|
+
const minGapMs = options.minGapMs ?? SESSION_PROBE_MIN_GAP_MS;
|
|
56
|
+
const now = options.now ?? (() => Date.now());
|
|
57
|
+
let lastProbeAt = null;
|
|
58
|
+
let probeInFlight = false;
|
|
59
|
+
return {
|
|
60
|
+
async trigger() {
|
|
61
|
+
if (!shouldProbeSession({
|
|
62
|
+
gate: options.readGate(),
|
|
63
|
+
probeInFlight,
|
|
64
|
+
lastProbeAt,
|
|
65
|
+
now: now(),
|
|
66
|
+
minGapMs,
|
|
67
|
+
})) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// The gap counts from probe START so a slow probe doesn't schedule the
|
|
71
|
+
// next one early; the in-flight flag covers the window until it settles.
|
|
72
|
+
lastProbeAt = now();
|
|
73
|
+
probeInFlight = true;
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
result = await options.probe();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Contract violation (probeSession must not throw) — treat as
|
|
80
|
+
// unreachable: silence, never a sign-out.
|
|
81
|
+
result = 'unreachable';
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
probeInFlight = false;
|
|
85
|
+
}
|
|
86
|
+
if (result === 'invalid')
|
|
87
|
+
options.onInvalid();
|
|
88
|
+
// 'valid' → nothing to do; 'unreachable' → silent by ratified scope:
|
|
89
|
+
// the token survived (adapter contract) and the operations layer owns
|
|
90
|
+
// backend-unavailability surfacing (EC-362/363).
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=session-revalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-revalidation.js","sourceRoot":"","sources":["../../src/lib/session-revalidation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAA;AAE9C,iFAAiF;AACjF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,MAAM,CAAA;AAYnD;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAMlC;IACC,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IACjE,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IACjE,IAAI,aAAa;QAAE,OAAO,KAAK,CAAA;IAC/B,IAAI,WAAW,KAAK,IAAI,IAAI,GAAG,GAAG,WAAW,GAAG,QAAQ;QAAE,OAAO,KAAK,CAAA;IACtE,OAAO,IAAI,CAAA;AACb,CAAC;AAwBD;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAkC;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,wBAAwB,CAAA;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC7C,IAAI,WAAW,GAAkB,IAAI,CAAA;IACrC,IAAI,aAAa,GAAG,KAAK,CAAA;IAEzB,OAAO;QACL,KAAK,CAAC,OAAO;YACX,IACE,CAAC,kBAAkB,CAAC;gBAClB,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE;gBACxB,aAAa;gBACb,WAAW;gBACX,GAAG,EAAE,GAAG,EAAE;gBACV,QAAQ;aACT,CAAC,EACF,CAAC;gBACD,OAAM;YACR,CAAC;YACD,uEAAuE;YACvE,yEAAyE;YACzE,WAAW,GAAG,GAAG,EAAE,CAAA;YACnB,aAAa,GAAG,IAAI,CAAA;YACpB,IAAI,MAA0B,CAAA;YAC9B,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;gBAC9D,0CAA0C;gBAC1C,MAAM,GAAG,aAAa,CAAA;YACxB,CAAC;oBAAS,CAAC;gBACT,aAAa,GAAG,KAAK,CAAA;YACvB,CAAC;YACD,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,CAAC,SAAS,EAAE,CAAA;YAC7C,qEAAqE;YACrE,sEAAsE;YACtE,iDAAiD;QACnD,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { SessionProbeResult } from './auth'\n\n/**\n * Proactive session re-validation scheduler (EC-415) — the pure, framework-free\n * core behind the window-focus/interval probes wired up in\n * `components/auth-context.tsx`.\n *\n * The adapters' tri-state `probeSession()` makes proactive checking SAFE\n * (`unreachable` never clears the stored token — see `lib/auth/auth-adapter.ts`);\n * this core makes it CHEAP and QUIET:\n *\n * - **Min gap:** at most one probe per {@link SESSION_PROBE_MIN_GAP_MS}\n * (30 s), no matter how many focus/visibility/interval triggers fire — alt-\n * tabbing back and forth never hammers the backend.\n * - **In-flight dedupe:** never two concurrent probes.\n * - **Gating:** probes run only over a live session (`signedIn`), never while\n * the re-auth overlay is already up (`blocked` — nothing left to detect),\n * and only while the tab is visible (`visible` — a background tab's session\n * dying is caught by the focus probe the moment the editor returns).\n * - **Routing:** `invalid` → {@link SessionRevalidatorOptions.onInvalid} (the\n * EC-403 chokepoint, `raiseSessionExpiry`); `valid` and `unreachable` are\n * silent. A probe that THROWS (contract violation) is treated as\n * `unreachable` — a scheduler must never turn a transport surprise into a\n * sign-out.\n *\n * Trigger cadence (the React wiring): window `focus` + document\n * `visibilitychange` fire a trigger immediately (subject to the min gap), and\n * a {@link SESSION_PROBE_INTERVAL_MS} (5 min) interval covers the\n * tab-left-open-and-visible case. Proactive detection is a UX refinement on\n * top of the reactive EC-403 chokepoint (any operation against a dead session\n * still raises within one call), so the cadence is deliberately modest.\n */\n\n/** At most one probe per this many milliseconds, across ALL trigger sources. */\nexport const SESSION_PROBE_MIN_GAP_MS = 30_000\n\n/** Background probe cadence while the tab stays visible (the React interval). */\nexport const SESSION_PROBE_INTERVAL_MS = 5 * 60_000\n\n/** The live studio state a probe is gated on (read fresh per trigger). */\nexport interface SessionRevalidatorGate {\n /** A session exists to re-validate (`status === 'signed-in'`). */\n readonly signedIn: boolean\n /** The re-auth overlay is already up — nothing left to detect. */\n readonly blocked: boolean\n /** The tab is visible; hidden tabs wait for the return-focus probe. */\n readonly visible: boolean\n}\n\n/**\n * Pure per-trigger decision: should this trigger actually probe? Exported\n * separately so the gating matrix is unit-pinnable without a scheduler.\n */\nexport function shouldProbeSession(input: {\n gate: SessionRevalidatorGate\n probeInFlight: boolean\n lastProbeAt: number | null\n now: number\n minGapMs: number\n}): boolean {\n const { gate, probeInFlight, lastProbeAt, now, minGapMs } = input\n if (!gate.signedIn || gate.blocked || !gate.visible) return false\n if (probeInFlight) return false\n if (lastProbeAt !== null && now - lastProbeAt < minGapMs) return false\n return true\n}\n\nexport interface SessionRevalidatorOptions {\n /** The adapter probe (`auth.probeSession()`). */\n probe: () => Promise<SessionProbeResult>\n /** Funnel for an `invalid` answer — the EC-403 raise (`raiseSessionExpiry`). */\n onInvalid: () => void\n /** Read the CURRENT gate state (refs, not a snapshot) at trigger time. */\n readGate: () => SessionRevalidatorGate\n /** Override {@link SESSION_PROBE_MIN_GAP_MS} (tests). */\n minGapMs?: number\n /** Clock override (tests). */\n now?: () => number\n}\n\nexport interface SessionRevalidator {\n /**\n * Fire from any trigger source (focus, visibilitychange, interval tick).\n * Resolves once the trigger is handled — immediately when gated away,\n * after the probe settles when one runs.\n */\n trigger(): Promise<void>\n}\n\n/**\n * Create the scheduler. One instance per auth adapter (the React effect's\n * lifetime); all internal state — last probe time, in-flight flag — lives\n * here, keeping the React wiring a thin listener layer.\n */\nexport function createSessionRevalidator(options: SessionRevalidatorOptions): SessionRevalidator {\n const minGapMs = options.minGapMs ?? SESSION_PROBE_MIN_GAP_MS\n const now = options.now ?? (() => Date.now())\n let lastProbeAt: number | null = null\n let probeInFlight = false\n\n return {\n async trigger(): Promise<void> {\n if (\n !shouldProbeSession({\n gate: options.readGate(),\n probeInFlight,\n lastProbeAt,\n now: now(),\n minGapMs,\n })\n ) {\n return\n }\n // The gap counts from probe START so a slow probe doesn't schedule the\n // next one early; the in-flight flag covers the window until it settles.\n lastProbeAt = now()\n probeInFlight = true\n let result: SessionProbeResult\n try {\n result = await options.probe()\n } catch {\n // Contract violation (probeSession must not throw) — treat as\n // unreachable: silence, never a sign-out.\n result = 'unreachable'\n } finally {\n probeInFlight = false\n }\n if (result === 'invalid') options.onInvalid()\n // 'valid' → nothing to do; 'unreachable' → silent by ratified scope:\n // the token survived (adapter contract) and the operations layer owns\n // backend-unavailability surfacing (EC-362/363).\n },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elytracms/studio",
|
|
3
3
|
"author": "finaldream <hi@finaldream.de>",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.66",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"lucide-react": "^1.23.0",
|
|
147
147
|
"tailwind-merge": "^3.6.0",
|
|
148
148
|
"zod": "^4.4.3",
|
|
149
|
-
"@elytracms/core": "^0.0.
|
|
149
|
+
"@elytracms/core": "^0.0.66"
|
|
150
150
|
},
|
|
151
151
|
"peerDependencies": {
|
|
152
152
|
"@tanstack/react-router": "^1.170.0",
|