@glance-apps/sync 1.6.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/types/index.d.ts CHANGED
@@ -27,7 +27,20 @@ export type SyncErrorCode =
27
27
  | 'VERIFIER_UNSUPPORTED'
28
28
  // DB transport: a row-scoped call (incl. the key verifier) was made before the
29
29
  // accountId was populated — retryable once the account id is available.
30
- | 'ACCOUNT_ID_REQUIRED';
30
+ | 'ACCOUNT_ID_REQUIRED'
31
+ // DB transport, per-account auth: the server rejected this device's
32
+ // credential (401 "invalid credential"). Surfaced with isHardStop === true;
33
+ // the engine persists a halt and stops retrying. Recovery (re-enrollment)
34
+ // is Phase 2.2 — nothing in this version clears the halt.
35
+ | 'CREDENTIAL_INVALID'
36
+ // DB transport: the server rejected a write because the account is over a
37
+ // configured quota (413 storage-shaped / 429 volume-shaped, Phase 3.2).
38
+ // ALWAYS isHardStop === false — a quota condition clears when the operator
39
+ // raises the limit or reclaim runs, with NO client action, so the engine
40
+ // never halts on it. The parsed descriptor rides on the engine's
41
+ // getQuotaState(); the engine suppresses further writes for a bounded,
42
+ // self-resuming window while the pull keeps running.
43
+ | 'QUOTA_EXCEEDED';
31
44
 
32
45
  export type SyncStatus = 'idle' | 'uploading' | 'downloading' | 'success' | 'error';
33
46
 
@@ -421,10 +434,53 @@ export interface VaultClient {
421
434
 
422
435
  export function createVaultClient(config: {
423
436
  vaultUrl: string;
437
+ /** Shared device token, or the device's enrolled credential in per-account mode. */
424
438
  vaultToken: string;
425
439
  fetchImpl?: typeof fetch;
426
440
  }): VaultClient;
427
441
 
442
+ // Per-account auth (vault Phase 1.4b).
443
+
444
+ /** Which trust model a GLANCEvault server runs. Discoverable via fetchVaultHealth. */
445
+ export type VaultAuthMode = 'shared' | 'per-account';
446
+
447
+ export interface VaultHealth {
448
+ status: string;
449
+ version: string;
450
+ schemaVersion: number;
451
+ /** Normalized to 'shared' when the server predates the field. */
452
+ authMode: VaultAuthMode;
453
+ }
454
+
455
+ /** Unauthenticated GET /healthz. Safe to call before any token exists. */
456
+ export function fetchVaultHealth(config: {
457
+ vaultUrl: string;
458
+ fetchImpl?: typeof fetch;
459
+ }): Promise<VaultHealth>;
460
+
461
+ export interface VaultEnrollment {
462
+ credentialId: string;
463
+ /** Returned once, never retrievable again. Persist it, then discard the bootstrap secret. */
464
+ credential: string;
465
+ accountId: string;
466
+ deviceId: string;
467
+ createdAt: string;
468
+ }
469
+
470
+ /**
471
+ * POST /enroll — exchanges the bootstrap secret for this device's own credential.
472
+ * Rejects with err.code 'ENROLLMENT_REJECTED' (401, secret not accepted),
473
+ * 'ENROLLMENT_UNSUPPORTED' (404, server runs shared mode or predates enrollment),
474
+ * or 'VAULT_ERROR' with err.status for other failures.
475
+ */
476
+ export function enrollVaultDevice(config: {
477
+ vaultUrl: string;
478
+ enrollmentSecret: string;
479
+ accountId: string;
480
+ deviceId: string;
481
+ fetchImpl?: typeof fetch;
482
+ }): Promise<VaultEnrollment>;
483
+
428
484
  export interface DbSyncEngineConfig {
429
485
  // Identity / local state
430
486
  storageKeyPrefix: string;
@@ -471,6 +527,74 @@ export interface DbSyncResult {
471
527
  applied: number;
472
528
  skipped: number;
473
529
  skippedEntityIds: string[];
530
+ /** True when the push threw but the pull still ran (Phase 3.3 partial function). */
531
+ pushFailed?: boolean;
532
+ /** True when the push was skipped because its backoff window is open. */
533
+ pushSkipped?: boolean;
534
+ /** The push error's code when pushFailed/pushSkipped — the pull's own result is still reported above. */
535
+ pushErrorCode?: SyncErrorCode | string;
536
+ /** True when the pull threw but the cursor report still ran. */
537
+ pullFailed?: boolean;
538
+ /** True when the pull was skipped because its backoff window is open. */
539
+ pullSkipped?: boolean;
540
+ /** The pull error's code when pullFailed/pullSkipped. */
541
+ pullErrorCode?: SyncErrorCode | string;
542
+ /** The standing over-quota descriptor, when one applies. */
543
+ quota?: QuotaState | null;
544
+ /** True when the cycle no-opped because the credential halt is set. */
545
+ halted?: boolean;
546
+ /** True when the cycle no-opped because this instance's credential was superseded. */
547
+ superseded?: boolean;
548
+ }
549
+
550
+ /**
551
+ * Over-quota state (Phase 3.3). IN MEMORY ONLY — this describes server-side
552
+ * state that changes without the client (an operator raises a limit, reclaim
553
+ * runs, intents expire), so it is never persisted and there is nothing to
554
+ * clear: it lifts by itself when a write succeeds again.
555
+ *
556
+ * `quota` is the server's dimension string, passed through verbatim — the
557
+ * known values are 'storage' | 'rows' | 'intents' | 'concurrent-uploads', but
558
+ * a newer server may add dimensions, so treat an unrecognised one generically
559
+ * rather than assuming it cannot happen. The numbers are bytes for 'storage'
560
+ * and counts otherwise, and render "X of Y used" directly; they are null only
561
+ * in the defensive case where a rejection was typed without a full body.
562
+ */
563
+ /**
564
+ * One backoff window. The DB engine keeps two (push and pull) and enforces
565
+ * them itself; both are IN MEMORY ONLY, like the file tier's, so a restart
566
+ * clears them — a relaunch is a user saying "try now", and a persisting
567
+ * failure re-opens the window immediately.
568
+ *
569
+ * A window only ever DELAYS: the next cycle after `until` probes again, with
570
+ * no user action and no restart. Failures that must not be delayed at all
571
+ * (PASSPHRASE_REQUIRED / ACCOUNT_ID_REQUIRED, which clear the moment the app
572
+ * supplies the value) and failures owned by another state (CREDENTIAL_INVALID,
573
+ * which halts) never open one.
574
+ */
575
+ export interface BackoffWindow {
576
+ /** Epoch ms; the direction is skipped while this is in the future. 0 when clear. */
577
+ until: number;
578
+ /** Consecutive failures driving the escalation (30s doubling; 15 min push cap, 5 min pull cap). */
579
+ strikes: number;
580
+ /** 'quota' (keeps surfacing its descriptor), 'auth' (flat hour on a 401), 'transport' (quiet after one signal). */
581
+ reason: 'quota' | 'auth' | 'transport' | null;
582
+ /** The typed error code that opened the window. */
583
+ code: SyncErrorCode | string | null;
584
+ /** ISO timestamp of the most recent failure. */
585
+ since: string | null;
586
+ }
587
+
588
+ export interface QuotaState {
589
+ quota: string;
590
+ limit: number | null;
591
+ used: number | null;
592
+ requested: number | null;
593
+ message: string;
594
+ /** ISO timestamp of the most recent rejection. */
595
+ since: string;
596
+ /** ISO timestamp after which the engine probes again, on its own. */
597
+ retryAt: string;
474
598
  }
475
599
 
476
600
  /** A row quarantined after failing to decrypt under the (verified) account key. */
@@ -510,7 +634,93 @@ export interface DbSyncEngine {
510
634
  isKeyVerified(): boolean;
511
635
  hasEncryptionReady(): boolean;
512
636
 
637
+ /** True while the persisted credential-rejected stop is set (CREDENTIAL_INVALID). */
638
+ isCredentialHalted(): boolean;
639
+ /** The persisted stop record, or null. Cleared only by recoverVaultSyncEngine after a verified successful re-enrollment. */
640
+ getCredentialHalt(): { message: string; at: string } | null;
641
+ /** True once this instance proved its bearer was superseded by recovery and went inert (in-memory only). */
642
+ isSuperseded(): boolean;
643
+ /** The standing over-quota descriptor, or null. In-memory only; never a hard stop; nothing to clear. */
644
+ getQuotaState(): QuotaState | null;
645
+ /** True while the bounded over-quota window is open (writes skipped, pulls continue). */
646
+ isQuotaSuppressed(): boolean;
647
+
648
+ /**
649
+ * Epoch ms before which the push/pull will not be attempted, or 0. Same
650
+ * getter names and meaning as the file engine's, so one scheduler can treat
651
+ * both tiers identically — but unlike the file tier, the DB engine also
652
+ * ENFORCES these itself, so honouring them is an optimisation, not a duty.
653
+ */
654
+ getUploadBackoffUntil(): number;
655
+ getDownloadBackoffUntil(): number;
656
+ /**
657
+ * The full backoff picture. `reason` distinguishes a one-hour auth window
658
+ * from a thirty-second transport one without inferring from the timestamp's
659
+ * magnitude, which is what an app needs to tell a user why and for how long.
660
+ */
661
+ getBackoffState(): { push: BackoffWindow; pull: BackoffWindow };
662
+ /** The resolved stable device identity this engine runs as. */
663
+ deviceId: string;
664
+
513
665
  vault: VaultClient;
514
666
  }
515
667
 
516
668
  export function createDbSyncEngine(config: DbSyncEngineConfig): DbSyncEngine;
669
+
670
+ /**
671
+ * Package-owned stable device identifier: generated once (crypto.randomUUID)
672
+ * and persisted under `{prefix}-device-id`. Used for both the device cursor
673
+ * and per-account enrollment. An explicit config.deviceId overrides it.
674
+ */
675
+ export function getOrCreateDeviceId(storageKeyPrefix: string): string;
676
+
677
+ /**
678
+ * The packaged vault connect flow: discovers the server's auth mode, resolves
679
+ * the Bearer value (enrolling with the bootstrap secret when the server is
680
+ * per-account and no credential is stored yet), persists the credential under
681
+ * `{prefix}-vault-credential`, and returns a ready engine. The bootstrap
682
+ * secret is never written to storage, never logged, and never retained past
683
+ * the call. No path re-enrolls automatically once a credential is stored.
684
+ *
685
+ * Typed failures: VAULT_TOKEN_REQUIRED, ENROLLMENT_SECRET_REQUIRED,
686
+ * CREDENTIAL_PERSIST_FAILED, VAULT_UNREACHABLE, plus enrollVaultDevice's
687
+ * ENROLLMENT_REJECTED / ENROLLMENT_UNSUPPORTED.
688
+ */
689
+ export function connectVaultSyncEngine(
690
+ config: Omit<DbSyncEngineConfig, 'vaultToken'> & {
691
+ vaultToken?: string;
692
+ enrollmentSecret?: string;
693
+ }
694
+ ): Promise<{
695
+ engine: DbSyncEngine;
696
+ /** null when discovery failed and the flow fell back to the last known auth state. */
697
+ authMode: VaultAuthMode | null;
698
+ /** True only when this call minted a credential. */
699
+ enrolled: boolean;
700
+ deviceId: string;
701
+ }>;
702
+
703
+ /**
704
+ * The exit from the credential halt (Phase 2.2): USER-INITIATED re-enrollment
705
+ * with the bootstrap secret. Refuses unless the device is halted (NOT_HALTED)
706
+ * and unless the server runs per-account auth (RECOVERY_UNSUPPORTED /
707
+ * VAULT_UNREACHABLE — recovery never falls back on discovery failure). The
708
+ * stale record's deviceId is used for enrollment so the server (2.1) revokes
709
+ * every still-active predecessor; an explicit config.deviceId differing from
710
+ * it is DEVICE_ID_CONFLICT. Order: canary -> enroll -> persist + verify
711
+ * (overwriting the stale record) -> clear the halt LAST -> fresh engine.
712
+ * Every failure leaves the device halted with its state intact. The secret is
713
+ * confined to the call, exactly as in connectVaultSyncEngine. Call only from
714
+ * a deliberate user action; no code path in this package invokes it.
715
+ */
716
+ export function recoverVaultSyncEngine(
717
+ config: Omit<DbSyncEngineConfig, 'vaultToken'> & {
718
+ vaultToken?: string;
719
+ enrollmentSecret: string;
720
+ }
721
+ ): Promise<{
722
+ engine: DbSyncEngine;
723
+ authMode: 'per-account';
724
+ enrolled: true;
725
+ deviceId: string;
726
+ }>;