@tinycloud/sdk-services 3.2.6-beta.4 → 3.3.0-beta.25
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/{BaseService-1Ug6y7DC.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
- package/dist/{BaseService-Bn07Vudy.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
- package/dist/encryption/index.cjs +25 -4
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +25 -4
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +959 -808
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -7
- package/dist/index.d.ts +114 -7
- package/dist/index.js +958 -808
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +136 -99
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +4 -5
- package/dist/kv/index.d.ts +4 -5
- package/dist/kv/index.js +136 -99
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/grantPolicy.cjs +82 -0
- package/dist/kv/replication/grantPolicy.cjs.map +1 -0
- package/dist/kv/replication/grantPolicy.d.cts +19 -0
- package/dist/kv/replication/grantPolicy.d.ts +19 -0
- package/dist/kv/replication/grantPolicy.js +50 -0
- package/dist/kv/replication/grantPolicy.js.map +1 -0
- package/dist/kv/replication/index.cjs +576 -132
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +3 -3
- package/dist/kv/replication/index.d.ts +3 -3
- package/dist/kv/replication/index.js +576 -132
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/{scope-BnyVPZ5E.d.ts → scope-9U4gVNFN.d.ts} +312 -7
- package/dist/{scope-CQ5kmHlt.d.cts → scope-BzdcrOZG.d.cts} +312 -7
- package/dist/sql/index.cjs +98 -50
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +98 -50
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-gmdUiorz.d.cts → types-eVS1Whuf.d.cts} +29 -3
- package/dist/{types-gmdUiorz.d.ts → types-eVS1Whuf.d.ts} +29 -3
- package/package.json +16 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Result,
|
|
1
|
+
import { e as RetryStats, R as Result, f as FetchFunction } from './types-eVS1Whuf.cjs';
|
|
2
2
|
import { REPLICA_KV_STORES } from './kv/replication/contractConstants.cjs';
|
|
3
3
|
import { ReplicaConsentAnchor, ReplicaGrantPolicy } from './kv/replication/grantPolicyTypes.cjs';
|
|
4
4
|
|
|
@@ -334,6 +334,8 @@ interface KVResponse<T = unknown> {
|
|
|
334
334
|
* Response headers with metadata.
|
|
335
335
|
*/
|
|
336
336
|
headers: KVResponseHeaders;
|
|
337
|
+
/** Attempts and total backoff for the request behind this response; see `RetryPolicy`. */
|
|
338
|
+
retry?: RetryStats;
|
|
337
339
|
}
|
|
338
340
|
/**
|
|
339
341
|
* Response from KV list operations.
|
|
@@ -526,7 +528,7 @@ interface ReplicaAccountGate {
|
|
|
526
528
|
/** Active account's principal hash; null after sign-out. */
|
|
527
529
|
principalHash: string | null;
|
|
528
530
|
lifecycleEpoch: number;
|
|
529
|
-
/** ms since epoch of the last deny (browser: preserved across database recreation). */
|
|
531
|
+
/** ms since epoch of the last committed deny (browser: preserved across database recreation). */
|
|
530
532
|
deniedAt: number | null;
|
|
531
533
|
/** Browser: finite ms, fixed from verified fresh consent at establish; mandatory while active. */
|
|
532
534
|
consentDeadline?: number | null;
|
|
@@ -665,8 +667,9 @@ type ReplicaDenyOutcome =
|
|
|
665
667
|
/**
|
|
666
668
|
* Gate moved, intents recorded, first-install anchors cleared and pending generations advanced
|
|
667
669
|
* in one commit. Browser: owner device deleted in that same IDB transaction. Node: the gate
|
|
668
|
-
* file cannot commit atomically with the device file, so
|
|
669
|
-
* identity's pending-intent erase before
|
|
670
|
+
* file cannot commit atomically with the device file, so an ordinary denial deletes it
|
|
671
|
+
* through the identity's pending-intent erase before intent clear. Deferred node sign-out
|
|
672
|
+
* holds that intent and the device file until validated resume (CONTRACT.md §2).
|
|
670
673
|
*/
|
|
671
674
|
{
|
|
672
675
|
status: "denied";
|
|
@@ -691,6 +694,15 @@ type ReplicaDenyOutcome =
|
|
|
691
694
|
status: "unguarded";
|
|
692
695
|
code: string;
|
|
693
696
|
};
|
|
697
|
+
/** A deferred sign-out denial: a committed `denied` always carries its resume token. */
|
|
698
|
+
type ProfileReplicaDeferredDenyOutcome = {
|
|
699
|
+
status: "denied";
|
|
700
|
+
lifecycleEpoch: number;
|
|
701
|
+
intents: number;
|
|
702
|
+
resumeToken: ProfileReplicaDeferredToken;
|
|
703
|
+
} | Exclude<ReplicaDenyOutcome, {
|
|
704
|
+
status: "denied";
|
|
705
|
+
}>;
|
|
694
706
|
interface ReplicaEraseReport {
|
|
695
707
|
erased: string[];
|
|
696
708
|
failed: Array<{
|
|
@@ -699,6 +711,11 @@ interface ReplicaEraseReport {
|
|
|
699
711
|
}>;
|
|
700
712
|
/** Intents still outstanding after this pass. */
|
|
701
713
|
remaining: number;
|
|
714
|
+
/**
|
|
715
|
+
* Node/CLI: a durable deferred denial held this pass, so nothing was erased and
|
|
716
|
+
* `remaining` (possibly 0) is not completion; check `deferredErase()`.
|
|
717
|
+
*/
|
|
718
|
+
deferred?: true;
|
|
702
719
|
/** Browser startup discovered elapsed consent and durably crypto-shredded the account/gate keys. */
|
|
703
720
|
consentExpired?: boolean;
|
|
704
721
|
}
|
|
@@ -781,6 +798,8 @@ interface ReplicaLifecycleGate {
|
|
|
781
798
|
* if expired, denies and crypto-shreds keys before any replica access; it also retries a
|
|
782
799
|
* pending `deleteDatabase` marker and erases intents. Failures remain gated and visible via
|
|
783
800
|
* `pendingPurge()` / `purgePending`; a browser closed at expiry catches it on its next load.
|
|
801
|
+
* Node/CLI recovery leaves durably deferred revoke intents and physical files untouched,
|
|
802
|
+
* reporting them as remaining until `ProfileReplicaPurge.resumeDeferredErase`.
|
|
784
803
|
*/
|
|
785
804
|
recover(o: {
|
|
786
805
|
signal: AbortSignal;
|
|
@@ -830,6 +849,12 @@ interface ReplicaLifecycleGate {
|
|
|
830
849
|
* return null when its gate has no persisted consent deadline.
|
|
831
850
|
*/
|
|
832
851
|
consentDeadline(stamp: ReplicaSessionStamp): number | null;
|
|
852
|
+
/**
|
|
853
|
+
* Browser-only post-commit consent expiry observer. Fires only after deny/delete
|
|
854
|
+
* destroys the origin account keys; a subscriber failure cannot undo denial.
|
|
855
|
+
* Controllers register after binding so events use their opened opaque replica IDs.
|
|
856
|
+
*/
|
|
857
|
+
subscribeConsentExpired?(listener: (stamp: ReplicaSessionStamp) => void): () => void;
|
|
833
858
|
/**
|
|
834
859
|
* Synchronous main-thread delivery check before replica-served values/list keys/event fields,
|
|
835
860
|
* and before open, installGrant and sync. Browser checks marker, origin deny tombstone,
|
|
@@ -853,6 +878,275 @@ interface ReplicaLifecycleGate {
|
|
|
853
878
|
/** Outstanding intents; non-empty means status `purgePending: true`. */
|
|
854
879
|
pendingPurge(): Promise<ReplicaPurgeIntent[]>;
|
|
855
880
|
}
|
|
881
|
+
/** Node/CLI profile-local fencing token, not a secret capability; the profile lock remains authoritative. */
|
|
882
|
+
interface ProfileReplicaDeferredToken {
|
|
883
|
+
deniedEpoch: number;
|
|
884
|
+
principalHash: string;
|
|
885
|
+
}
|
|
886
|
+
/** Profile-relative, JSON-safe identity of a path listed before destructive logout. */
|
|
887
|
+
interface ProfileReplicaUnknownEntry {
|
|
888
|
+
path: string;
|
|
889
|
+
kind: "file" | "directory" | "symlink";
|
|
890
|
+
dev: string;
|
|
891
|
+
ino: string;
|
|
892
|
+
}
|
|
893
|
+
/** Profile-relative directory identity; dev/ino are decimal strings for durable JSON. */
|
|
894
|
+
interface ProfileReplicaRootIdentity {
|
|
895
|
+
path: string;
|
|
896
|
+
dev: string;
|
|
897
|
+
ino: string;
|
|
898
|
+
}
|
|
899
|
+
/** Exact nonsecret path identity displayed with an inventory confirmation token. */
|
|
900
|
+
interface ProfileReplicaApprovalEntry extends ProfileReplicaRootIdentity {
|
|
901
|
+
kind: ProfileReplicaUnknownEntry["kind"] | "missing";
|
|
902
|
+
}
|
|
903
|
+
/** Read-only profile-locked inventory, including legacy mirrors and flag-owned roots. */
|
|
904
|
+
interface ProfileReplicaPreflight {
|
|
905
|
+
knownLegacyReplicas: number;
|
|
906
|
+
unknownLegacyPaths: string[];
|
|
907
|
+
unknownPaths: string[];
|
|
908
|
+
unknownEntries: ProfileReplicaUnknownEntry[];
|
|
909
|
+
knownRoots: ProfileReplicaRootIdentity[];
|
|
910
|
+
knownEntries: ProfileReplicaUnknownEntry[];
|
|
911
|
+
missingDatabases: string[];
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* CLI-only durable crash fence. `principalDigest` is a domain-separated
|
|
915
|
+
* SHA-256 owner binding, not a secret or a substitute for the profile lock;
|
|
916
|
+
* v2/v3 never fabricate a scoped gate principal hash. A v1 `lifecycleEpoch`
|
|
917
|
+
* is the pre-denial epoch; `deniedEpoch` is the epoch the v1 denial actually
|
|
918
|
+
* committed, bound once after that denial and compared for equality from then
|
|
919
|
+
* on (CONTRACT.md §2). Remote-pending and remote-abandoned are mutually
|
|
920
|
+
* exclusive durable states.
|
|
921
|
+
*/
|
|
922
|
+
type ProfileReplicaLogoutPending = ({
|
|
923
|
+
v: 1;
|
|
924
|
+
incarnation: string;
|
|
925
|
+
lifecycleEpoch: number;
|
|
926
|
+
principalHash: string;
|
|
927
|
+
deniedEpoch?: number;
|
|
928
|
+
} | {
|
|
929
|
+
v: 2;
|
|
930
|
+
scope: "orphan_profile";
|
|
931
|
+
nonce: string;
|
|
932
|
+
} | {
|
|
933
|
+
v: 3;
|
|
934
|
+
scope: "inactive_gate";
|
|
935
|
+
incarnation: string;
|
|
936
|
+
lifecycleEpoch: number;
|
|
937
|
+
}) & ProfileReplicaRemoteLogoutState & {
|
|
938
|
+
principalDigest: string;
|
|
939
|
+
/** Random durable 16-byte challenge salt as 32 lowercase hex characters. */
|
|
940
|
+
approvalSalt: string;
|
|
941
|
+
discardLocalChanges?: true;
|
|
942
|
+
approvedUnknown: ProfileReplicaUnknownEntry[];
|
|
943
|
+
knownRoots: ProfileReplicaRootIdentity[];
|
|
944
|
+
knownEntries: ProfileReplicaUnknownEntry[];
|
|
945
|
+
missingDatabases: string[];
|
|
946
|
+
};
|
|
947
|
+
/** A logout marker is remote-pending, remote-abandoned or neither; never both. */
|
|
948
|
+
type ProfileReplicaRemoteLogoutState = {
|
|
949
|
+
remotePending: true;
|
|
950
|
+
remoteAbandoned?: never;
|
|
951
|
+
} | {
|
|
952
|
+
remoteAbandoned: true;
|
|
953
|
+
remotePending?: never;
|
|
954
|
+
} | {
|
|
955
|
+
remotePending?: never;
|
|
956
|
+
remoteAbandoned?: never;
|
|
957
|
+
};
|
|
958
|
+
/**
|
|
959
|
+
* Node/CLI-only lifecycle facade for a profile's replica root. The node-sdk exports
|
|
960
|
+
* `createProfileReplicaPurge({ profileDir, guard })`; `guard` is the caller's
|
|
961
|
+
* mandatory profile lock and MUST be reentrant within one async call chain: the
|
|
962
|
+
* CLI calls the facade while already holding its (reentrant) profile lock. This
|
|
963
|
+
* does not widen the browser `ReplicaLifecycleGate`.
|
|
964
|
+
*
|
|
965
|
+
* While a durable deferred denial is active, `recover` and `erasePending`
|
|
966
|
+
* leave intents and physical files untouched; `pendingPurge` lists the intents.
|
|
967
|
+
* A complete enumeration may have zero intents, so `remaining: 0` does not
|
|
968
|
+
* prove completion: `deferredErase()` is the authoritative marker check, and
|
|
969
|
+
* `LocalReplicaStatus.purgePending` / `ReplicaInspectedStatus.purgePending`
|
|
970
|
+
* are true while the deferred marker or any intent remains. All guarded reads
|
|
971
|
+
* and new establishment remain refused. Deferred intents leave that state only
|
|
972
|
+
* through a validated resume (CONTRACT.md §2); there is no account-switch or
|
|
973
|
+
* consent-expiry override. The facade never denies or erases a gate,
|
|
974
|
+
* partition, pending state or device record whose active principal is not
|
|
975
|
+
* the caller's ("Logout by another account", CONTRACT.md §2).
|
|
976
|
+
*/
|
|
977
|
+
interface ProfileReplicaPurge extends Pick<ReplicaLifecycleGate, "recover" | "erasePending" | "pendingPurge"> {
|
|
978
|
+
/**
|
|
979
|
+
* `deferErase: true` is valid only for `reason: "sign_out"`; other reasons throw
|
|
980
|
+
* TypeError before any mutation. A successful deferred `denied` outcome MUST carry
|
|
981
|
+
* `resumeToken`; stale or unguarded outcomes do not authorize a retryable revoke.
|
|
982
|
+
* Step 1 atomically denies, records every opaque partition/pending intent and
|
|
983
|
+
* pending generation, and persists the deferred marker. Physical files remain
|
|
984
|
+
* until `resumeDeferredErase`, even across process restart.
|
|
985
|
+
*/
|
|
986
|
+
deny(input: ReplicaDenyInput & {
|
|
987
|
+
reason: "sign_out";
|
|
988
|
+
}, options: {
|
|
989
|
+
deferErase: true;
|
|
990
|
+
}): Promise<ProfileReplicaDeferredDenyOutcome>;
|
|
991
|
+
deny(input: ReplicaDenyInput, options?: {
|
|
992
|
+
deferErase?: false;
|
|
993
|
+
}): Promise<ReplicaDenyOutcome>;
|
|
994
|
+
/** Returns the persisted active token across restart, even with zero intents; null only after the marker clears. */
|
|
995
|
+
deferredErase(): Promise<ProfileReplicaDeferredToken | null>;
|
|
996
|
+
/**
|
|
997
|
+
* Under the profile lock, require the exact denied epoch and principal hash and a
|
|
998
|
+
* null gate principal; otherwise throw `REPLICA_GATED`/`stamp_stale` without erasure.
|
|
999
|
+
* Clear the deferred marker durably before idempotent erase. A crash after clearing
|
|
1000
|
+
* is completed by ordinary recovery. Repeating the same token is safe only while
|
|
1001
|
+
* the same denied epoch/hash persists; a successor establishment fences it.
|
|
1002
|
+
*/
|
|
1003
|
+
resumeDeferredErase(token: ProfileReplicaDeferredToken, options: {
|
|
1004
|
+
signal: AbortSignal;
|
|
1005
|
+
}): Promise<ReplicaEraseReport>;
|
|
1006
|
+
/**
|
|
1007
|
+
* Read-only profile-locked inventory BEFORE marker, denial or remote call.
|
|
1008
|
+
* Report exact profile-relative unknown paths, kinds and dev/ino plus all
|
|
1009
|
+
* known roots/entries. Unknown/indeterminate data refuses unless explicit
|
|
1010
|
+
* discard consent was supplied; the CLI displays the inventory before acting.
|
|
1011
|
+
* A prior inventory is not a wildcard authorization for later new paths.
|
|
1012
|
+
*/
|
|
1013
|
+
preflightLogout(input: {
|
|
1014
|
+
principal: string;
|
|
1015
|
+
discardLocalChanges?: boolean;
|
|
1016
|
+
}): Promise<ProfileReplicaPreflight>;
|
|
1017
|
+
/**
|
|
1018
|
+
* Re-inspect and compare the complete approved inventory under the profile
|
|
1019
|
+
* lock, then atomically fsync a scoped v1, orphan v2 or inactive v3 marker.
|
|
1020
|
+
* `approval` is required: the CLI passes its just-displayed preflight even
|
|
1021
|
+
* when it contains no unknown paths. With `remotePending`, the marker fences
|
|
1022
|
+
* replica reads but forbids credential cleanup or erasure until
|
|
1023
|
+
* `completeRemoteLogout` after all exact-CID revokes. Only approved
|
|
1024
|
+
* path/type/dev/ino identities and the explicit discard bit survive; changed
|
|
1025
|
+
* paths refuse pre-action. V1 active remote logout additionally requires a
|
|
1026
|
+
* committed deferred denial/token before online revoke; v2 orphan and v3
|
|
1027
|
+
* inactive markers instead use their version-specific gate-absence/epoch
|
|
1028
|
+
* fences.
|
|
1029
|
+
*
|
|
1030
|
+
* An existing marker is never rewritten here: the same owner, fence,
|
|
1031
|
+
* `remotePending` state and approved inventory return it unchanged; anything
|
|
1032
|
+
* else refuses without mutation. When the gate's active principal is the
|
|
1033
|
+
* caller's, it always yields a v1 marker, even with no replica roots or
|
|
1034
|
+
* intents; another account's active gate yields null and is never denied.
|
|
1035
|
+
* Null otherwise means there is no active principal and no replica-owned
|
|
1036
|
+
* work, not completed logout.
|
|
1037
|
+
*
|
|
1038
|
+
* A missing flag pending index, unrecognized child, nonempty pending state
|
|
1039
|
+
* or corrupt gate requires explicit discard. The CLI still fsyncs
|
|
1040
|
+
* authSignedOut then unlinks and fsyncs both credentials. CLI-bound reads
|
|
1041
|
+
* and ordinary network restore refuse any marker; an explicit remote-revoke
|
|
1042
|
+
* retry may use the retained key/session while `remotePending` without
|
|
1043
|
+
* delivering replica data.
|
|
1044
|
+
*/
|
|
1045
|
+
prepareLogoutPending(input: {
|
|
1046
|
+
principal: string;
|
|
1047
|
+
approval: ProfileReplicaPreflight;
|
|
1048
|
+
discardLocalChanges?: boolean;
|
|
1049
|
+
remotePending?: true;
|
|
1050
|
+
}): Promise<ProfileReplicaLogoutPending | null>;
|
|
1051
|
+
/**
|
|
1052
|
+
* After every authoritative exact-CID revoke succeeds, revalidate the
|
|
1053
|
+
* marker's principal and version-specific fence under the profile lock:
|
|
1054
|
+
* v1 `deniedEpoch`/hash, v2 no replacement gate, or v3 exact inactive
|
|
1055
|
+
* epoch/null principal. Then durably remove `remotePending`. A marker that is
|
|
1056
|
+
* not remote-pending (remote-abandoned or already cleanup-ready) refuses
|
|
1057
|
+
* without mutation. A failed/partial remote sequence never calls this.
|
|
1058
|
+
*/
|
|
1059
|
+
completeRemoteLogout(input: {
|
|
1060
|
+
principal: string;
|
|
1061
|
+
}): Promise<ProfileReplicaLogoutPending>;
|
|
1062
|
+
/**
|
|
1063
|
+
* Under the profile lock, report whether this principal's marker is still
|
|
1064
|
+
* remote-pending and its v1 deferred gate, v2 orphan or v3 inactive gate
|
|
1065
|
+
* fence still matches. False only when the marker is absent/completed;
|
|
1066
|
+
* a mismatched principal or stale gate refuses. This permits an explicit
|
|
1067
|
+
* network-only revoke retry, never replica delivery.
|
|
1068
|
+
*/
|
|
1069
|
+
remotePendingLogout(input: {
|
|
1070
|
+
principal: string;
|
|
1071
|
+
}): Promise<boolean>;
|
|
1072
|
+
/**
|
|
1073
|
+
* First explicit PLAIN logout attempt after unfinished remote revoke:
|
|
1074
|
+
* compare the original durable approved inventory with the current
|
|
1075
|
+
* path/type/dev/ino set under the profile lock. Any change refuses with
|
|
1076
|
+
* `REPLICA_GATED`/`gate_unavailable`, prints every token-bound
|
|
1077
|
+
* `ProfileReplicaApprovalEntry`, exposes the same complete set as
|
|
1078
|
+
* `error.inventoryApprovalEntries` plus a nonsecret 32-hex-character
|
|
1079
|
+
* `error.inventoryApprovalToken`, and mutates nothing even when `approval`
|
|
1080
|
+
* contains the new paths or `discardLocalChanges` is true. With the
|
|
1081
|
+
* original inventory intact, validate the fresh approval and the marker's
|
|
1082
|
+
* version-specific owner/gate fence (a v1 marker is classified by its gate
|
|
1083
|
+
* and checked exactly one way, CONTRACT.md §2), then atomically replace
|
|
1084
|
+
* `remotePending` with `remoteAbandoned: true`.
|
|
1085
|
+
* This is not an online-success receipt and makes no further node call.
|
|
1086
|
+
*/
|
|
1087
|
+
abandonRemoteLogout(input: {
|
|
1088
|
+
principal: string;
|
|
1089
|
+
approval: ProfileReplicaPreflight;
|
|
1090
|
+
discardLocalChanges?: boolean;
|
|
1091
|
+
}): Promise<ProfileReplicaLogoutPending>;
|
|
1092
|
+
/**
|
|
1093
|
+
* Only a separate explicit CLI command with
|
|
1094
|
+
* `--discard-local-changes --approve-inventory <token>` may approve a
|
|
1095
|
+
* changed remote-pending inventory. Under the profile lock, require the
|
|
1096
|
+
* same owner and version-specific gate fence, original-to-current inventory
|
|
1097
|
+
* mismatch, exact fresh preflight and matching displayed 32-hex-character
|
|
1098
|
+
* token. Commit the newly approved identities with `remoteAbandoned: true`,
|
|
1099
|
+
* never a remote receipt. Missing, stale or reused tokens fail without
|
|
1100
|
+
* mutation; there is no same-command fallback from `abandonRemoteLogout`.
|
|
1101
|
+
*/
|
|
1102
|
+
approveChangedPlainLogout(input: {
|
|
1103
|
+
principal: string;
|
|
1104
|
+
approval: ProfileReplicaPreflight;
|
|
1105
|
+
discardLocalChanges: true;
|
|
1106
|
+
inventoryApprovalToken: string;
|
|
1107
|
+
}): Promise<ProfileReplicaLogoutPending>;
|
|
1108
|
+
/**
|
|
1109
|
+
* After a completed remote sequence, an inventory mismatch leaves the
|
|
1110
|
+
* marker and signed-out state in place. A fresh explicit listing/ack under
|
|
1111
|
+
* the profile lock replaces only the approved inventory; remote revokes
|
|
1112
|
+
* are not repeated, and a still-pending remote sequence cannot reapprove.
|
|
1113
|
+
*/
|
|
1114
|
+
reapproveLogoutPending(input: {
|
|
1115
|
+
principal: string;
|
|
1116
|
+
approval: ProfileReplicaPreflight;
|
|
1117
|
+
}): Promise<ProfileReplicaLogoutPending>;
|
|
1118
|
+
/**
|
|
1119
|
+
* CLI startup/logout recovery under the same profile lock. A `remotePending`
|
|
1120
|
+
* marker returns failure with no credential or file deletion. For a
|
|
1121
|
+
* cleanup-ready active-v1 marker, validate the gate and durably commit
|
|
1122
|
+
* generation-fenced denial with complete partition/pending intents
|
|
1123
|
+
* BEFORE calling `clearCredentials`; denial failure leaves the marker,
|
|
1124
|
+
* both credentials and replica files intact and reports nonzero failure.
|
|
1125
|
+
* An already-deferred v1 validates its exact denial; v3 validates its
|
|
1126
|
+
* inactive epoch and v2 its orphan/no-replacement-gate fence without
|
|
1127
|
+
* inventing scoped intents. Only then does `clearCredentials` atomically
|
|
1128
|
+
* fsync CLI authSignedOut before unlinking and fsyncing both session.json
|
|
1129
|
+
* and key.json. Recheck durable approved identities before each unlink;
|
|
1130
|
+
* mismatch stops with marker, remaining files and nonzero failure.
|
|
1131
|
+
* Post-success credentials remain shredded. The callback erases only its
|
|
1132
|
+
* approved inventory under the profile lock (never recursive legacy
|
|
1133
|
+
* deletion). In-repo CLI code owns the signed-out postcondition; the
|
|
1134
|
+
* facade verifies credential absence before erasure. A missing marker
|
|
1135
|
+
* with corrupt/unreadable gate reports failure and nonzero remaining,
|
|
1136
|
+
* without invoking callbacks or fabricating successful completion.
|
|
1137
|
+
*/
|
|
1138
|
+
recoverLogoutPending(options: {
|
|
1139
|
+
signal: AbortSignal;
|
|
1140
|
+
clearCredentials: () => Promise<void>;
|
|
1141
|
+
eraseLegacyReplicas: (options: {
|
|
1142
|
+
discardLocalChanges: boolean;
|
|
1143
|
+
approvedUnknown: readonly ProfileReplicaUnknownEntry[];
|
|
1144
|
+
knownRoots: readonly ProfileReplicaRootIdentity[];
|
|
1145
|
+
knownEntries: readonly ProfileReplicaUnknownEntry[];
|
|
1146
|
+
missingDatabases: readonly string[];
|
|
1147
|
+
}) => Promise<void>;
|
|
1148
|
+
}): Promise<ReplicaEraseReport>;
|
|
1149
|
+
}
|
|
856
1150
|
/** Worker replies that carry replica-served data are stamped; a reply without a stamp is dropped. */
|
|
857
1151
|
interface ReplicaStampedReply {
|
|
858
1152
|
stamp: ReplicaSessionStamp;
|
|
@@ -1315,10 +1609,13 @@ interface ReplicationAuthority {
|
|
|
1315
1609
|
mint(deviceDid: string, prefix: string, signal: AbortSignal,
|
|
1316
1610
|
/**
|
|
1317
1611
|
* `expiryMs` is a DURATION from now (forwarded as `delegateTo(…, { expiry: expiryMs })`), from
|
|
1318
|
-
* `mintRemainingMs`. Absent means Phase 1 behaviour (session-tier default).
|
|
1612
|
+
* `mintRemainingMs`. Absent means Phase 1 behaviour (session-tier default). `renewalOnly`
|
|
1613
|
+
* requires the authority to recheck a session-key parent at mint time; an initial runtime
|
|
1614
|
+
* parent mint leaves it unset.
|
|
1319
1615
|
*/
|
|
1320
1616
|
options?: {
|
|
1321
1617
|
expiryMs?: number;
|
|
1618
|
+
renewalOnly?: boolean;
|
|
1322
1619
|
}): Promise<{
|
|
1323
1620
|
ucan: string;
|
|
1324
1621
|
parentCid: string;
|
|
@@ -1425,6 +1722,12 @@ interface KVReadThrough {
|
|
|
1425
1722
|
}): void;
|
|
1426
1723
|
}
|
|
1427
1724
|
interface KVReplicationController extends KVReadThrough {
|
|
1725
|
+
/**
|
|
1726
|
+
* Internal browser gate observer: invoked only after the stamped durable consent-expiry
|
|
1727
|
+
* denial/deletion commits. Records opened opaque grant transitions even if the gate's own
|
|
1728
|
+
* timer wins before this controller's timer; does not issue a second deny.
|
|
1729
|
+
*/
|
|
1730
|
+
observeConsentExpired(stamp: ReplicaSessionStamp): void;
|
|
1428
1731
|
status(): Promise<ReplicaStatusEntry[]>;
|
|
1429
1732
|
sync(o?: {
|
|
1430
1733
|
prefix?: string;
|
|
@@ -1525,7 +1828,9 @@ interface ReplicaStatusEntry extends Partial<LocalReplicaStatus> {
|
|
|
1525
1828
|
type ReplicaState = "idle" | "ready" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked"
|
|
1526
1829
|
/** The lifecycle gate refuses this session (REPLICA_GATED); reads use the network. */
|
|
1527
1830
|
| "gated" | "closed";
|
|
1528
|
-
type ReplicationReason = "hit" | "absent" | "deleted" | "content_missing" | "coverage_incomplete" | "not_covered" | "pending_write" | "grant_missing" | "grant_expired"
|
|
1831
|
+
type ReplicationReason = "hit" | "absent" | "deleted" | "content_missing" | "coverage_incomplete" | "not_covered" | "pending_write" | "grant_missing" | "grant_expired"
|
|
1832
|
+
/** A revoked grant, including a stale-read sync refusal; read-through uses network with code GRANT_REVOKED. */
|
|
1833
|
+
| "grant_revoked" | "grant_not_yet_valid" | "stale" | "replica_error" | "runtime_unsupported" | "replica_unavailable" | "unsupported_option" | "network_cursor" | "cursor_restart" | "aborted" | "REPLICA_BEHIND_OWN_WRITES" | "REPLICA_UNPROVEN_SINCE_START" | "NETWORK_REQUESTED"
|
|
1529
1834
|
/** The lifecycle gate refused (REPLICA_GATED); the read went to the network. */
|
|
1530
1835
|
| "replica_gated";
|
|
1531
1836
|
type ReplicationEvent = {
|
|
@@ -1656,4 +1961,4 @@ declare function replicationIdentityKey(id: ReplicationIdentity): string;
|
|
|
1656
1961
|
/** Whether replicating this space/prefix can copy encrypted secret material to disk. */
|
|
1657
1962
|
declare function requiresSecretsOptIn(space: string, prefix: string): boolean;
|
|
1658
1963
|
|
|
1659
|
-
export { type
|
|
1964
|
+
export { type ReplicaGuardedStore as $, type AuthorityRefusal as A, type ReplicaBrowserGateAccess as B, CLEAR_PENDING_WARNING as C, type ReplicaBrowserGateSnapshot as D, type ReplicaBrowserGateTx as E, type ReplicaBrowserPageAdmission as F, type ReplicaClock as G, type ReplicaCoverage as H, type ReplicaDeliveryDecision as I, type ReplicaDenyInput as J, type KVReplicationDeps as K, type LocalGetResult as L, type ReplicaDenyOutcome as M, type ReplicaDevice as N, type ReplicaEraseReport as O, type PendingWriteStore as P, type ReplicaGateAccess as Q, type ReplicationIdentity as R, type ReplicaGateReason as S, type ReplicaGateScope as T, type ReplicaGateSnapshot as U, type ReplicaGateTx as V, type ReplicaGatedError as W, type ReplicaGatedHandle as X, type ReplicaGatedSpec as Y, type ReplicaGatedStorage as Z, type ReplicaGrantInfo as _, type KVReplicationController as a, type ReplicaInspectedStatus as a0, type ReplicaLifecycleGate as a1, type ReplicaPageAdmission as a2, type ReplicaPartitionEraser as a3, type ReplicaPartitionIndexEntry as a4, type ReplicaPeerCommit as a5, type ReplicaPrivateGateHash as a6, type ReplicaPurgeIntent as a7, type ReplicaQuotaLedger as a8, type ReplicaQuotaSpec as a9, type KVChangesResponse as aA, type KVCreateSignedReadUrlOptions as aB, type KVDeleteOptions as aC, type KVGetOptions as aD, type KVHeadOptions as aE, type KVListOptions as aF, type KVListResponse as aG, type KVPutOptions as aH, type KVResponse as aI, type KVResponseHeaders as aJ, type KVServiceConfig as aK, type KVSignedReadUrlResponse as aL, type ReplicaSessionStamp as aa, type ReplicaStampedReply as ab, type ReplicaState as ac, type ReplicaStatusEntry as ad, type ReplicationAuthority as ae, type ReplicationControl as af, type ReplicationEvent as ag, type ReplicationOptions as ah, type ReplicationPurgeReport as ai, type ReplicationReason as aj, type ReplicationScheduler as ak, type ResolvedReplicationOptions as al, type SaltedHash as am, canonicalReplicationIdentity as an, replicationIdentityKey as ao, requiresSecretsOptIn as ap, DEFAULT_SIGNED_READ_URL_EXPIRY_MS as aq, KVAction as ar, type KVActionType as as, type KVBatchPutItem as at, type KVBatchPutOptions as au, type KVBatchPutResponse as av, type KVBatchReadResponse as aw, type KVChange as ax, type KVChangesAuthority as ay, type KVChangesOptions as az, type KVListPage as b, type KVReadThrough as c, type KVReplicaHandle as d, type KVReplicaSpec as e, type KVReplicaStorage as f, type LocalListResult as g, type LocalReadMeta as h, type LocalReplicaStatus as i, type LocalSyncResult as j, type PendingWriteRecord as k, type PendingWriteState as l, type PinnedKey as m, type ProfileReplicaApprovalEntry as n, type ProfileReplicaDeferredDenyOutcome as o, type ProfileReplicaDeferredToken as p, type ProfileReplicaLogoutPending as q, type ProfileReplicaPreflight as r, type ProfileReplicaPurge as s, type ProfileReplicaRemoteLogoutState as t, type ProfileReplicaRootIdentity as u, type ProfileReplicaUnknownEntry as v, type PurgeTarget as w, type ReplicaAccountGate as x, type ReplicaAdmitHook as y, type ReplicaAuthorityState as z };
|
package/dist/sql/index.cjs
CHANGED
|
@@ -43,6 +43,14 @@ var ErrorCodes = {
|
|
|
43
43
|
ABORTED: "ABORTED",
|
|
44
44
|
INVALID_INPUT: "INVALID_INPUT",
|
|
45
45
|
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
46
|
+
// Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
|
|
47
|
+
// retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
|
|
48
|
+
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
49
|
+
NODE_BUSY: "NODE_BUSY",
|
|
50
|
+
/** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
|
|
51
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
52
|
+
/** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
|
|
53
|
+
OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
|
|
46
54
|
// KV-specific errors
|
|
47
55
|
KV_NOT_FOUND: "KV_NOT_FOUND",
|
|
48
56
|
KV_WRITE_FAILED: "KV_WRITE_FAILED",
|
|
@@ -78,11 +86,12 @@ var ErrorCodes = {
|
|
|
78
86
|
DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
|
|
79
87
|
};
|
|
80
88
|
var defaultRetryPolicy = {
|
|
81
|
-
maxAttempts:
|
|
89
|
+
maxAttempts: 4,
|
|
82
90
|
backoff: "exponential",
|
|
83
|
-
baseDelayMs:
|
|
84
|
-
maxDelayMs:
|
|
85
|
-
retryableErrors: [ErrorCodes.
|
|
91
|
+
baseDelayMs: 250,
|
|
92
|
+
maxDelayMs: 4e3,
|
|
93
|
+
retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
|
|
94
|
+
maxTotalMs: 12e3
|
|
86
95
|
};
|
|
87
96
|
var TelemetryEvents = {
|
|
88
97
|
SPAN: "telemetry.span",
|
|
@@ -118,6 +127,42 @@ function serviceError(code, message, service, options) {
|
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
// src/responseErrors.ts
|
|
131
|
+
function parseServiceErrorBody(errorText) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(errorText);
|
|
134
|
+
} catch {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
139
|
+
if (parsed.message) {
|
|
140
|
+
return compactMessage(parsed.message);
|
|
141
|
+
}
|
|
142
|
+
if (looksLikeHtml(errorText)) {
|
|
143
|
+
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
144
|
+
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
145
|
+
}
|
|
146
|
+
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
147
|
+
}
|
|
148
|
+
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
149
|
+
}
|
|
150
|
+
function responseErrorMeta(status, statusText, errorText) {
|
|
151
|
+
const meta = { status, statusText };
|
|
152
|
+
const snippet = compactMessage(errorText);
|
|
153
|
+
if (snippet) {
|
|
154
|
+
meta.responseSnippet = snippet.slice(0, 300);
|
|
155
|
+
}
|
|
156
|
+
return meta;
|
|
157
|
+
}
|
|
158
|
+
function looksLikeHtml(text) {
|
|
159
|
+
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
160
|
+
}
|
|
161
|
+
function compactMessage(message) {
|
|
162
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
163
|
+
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
// src/errors.ts
|
|
122
167
|
var import_bootstrap = require("@tinycloud/bootstrap");
|
|
123
168
|
function authRequiredError(service) {
|
|
@@ -127,6 +172,30 @@ function authRequiredError(service) {
|
|
|
127
172
|
service
|
|
128
173
|
};
|
|
129
174
|
}
|
|
175
|
+
function nodeRetryError(service, message, response, errorText) {
|
|
176
|
+
const body = errorText.trim();
|
|
177
|
+
const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
|
|
178
|
+
let code;
|
|
179
|
+
if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
|
|
180
|
+
code = ErrorCodes.NODE_BUSY;
|
|
181
|
+
} else if (response.status === 503 && token === "request_timeout") {
|
|
182
|
+
code = ErrorCodes.REQUEST_TIMEOUT;
|
|
183
|
+
} else if (response.status === 500 && token === "outcome_unknown") {
|
|
184
|
+
code = ErrorCodes.OUTCOME_UNKNOWN;
|
|
185
|
+
} else {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
|
|
189
|
+
const retryAfter = response.headers.get("retry-after")?.trim();
|
|
190
|
+
return serviceError(code, message, service, {
|
|
191
|
+
meta: {
|
|
192
|
+
...responseErrorMeta(response.status, response.statusText, errorText),
|
|
193
|
+
retryable,
|
|
194
|
+
...token === "sql_busy" && stage ? { stage } : {},
|
|
195
|
+
...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
130
199
|
var RequestTimeoutError = class extends Error {
|
|
131
200
|
constructor(timeoutMs) {
|
|
132
201
|
super(`Request timed out after ${timeoutMs}ms.`);
|
|
@@ -446,6 +515,18 @@ var BaseService = class {
|
|
|
446
515
|
}
|
|
447
516
|
return controller.signal;
|
|
448
517
|
}
|
|
518
|
+
/** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
|
|
519
|
+
invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
|
|
520
|
+
const build = () => {
|
|
521
|
+
const signed = mint();
|
|
522
|
+
return {
|
|
523
|
+
method: "POST",
|
|
524
|
+
...init,
|
|
525
|
+
headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
|
|
529
|
+
}
|
|
449
530
|
/**
|
|
450
531
|
* Create the abort signal for one request, bounded by the request timeout.
|
|
451
532
|
*
|
|
@@ -534,42 +615,6 @@ var BaseService = class {
|
|
|
534
615
|
}
|
|
535
616
|
};
|
|
536
617
|
|
|
537
|
-
// src/responseErrors.ts
|
|
538
|
-
function parseServiceErrorBody(errorText) {
|
|
539
|
-
try {
|
|
540
|
-
return JSON.parse(errorText);
|
|
541
|
-
} catch {
|
|
542
|
-
return {};
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
546
|
-
if (parsed.message) {
|
|
547
|
-
return compactMessage(parsed.message);
|
|
548
|
-
}
|
|
549
|
-
if (looksLikeHtml(errorText)) {
|
|
550
|
-
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
551
|
-
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
552
|
-
}
|
|
553
|
-
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
554
|
-
}
|
|
555
|
-
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
556
|
-
}
|
|
557
|
-
function responseErrorMeta(status, statusText, errorText) {
|
|
558
|
-
const meta = { status, statusText };
|
|
559
|
-
const snippet = compactMessage(errorText);
|
|
560
|
-
if (snippet) {
|
|
561
|
-
meta.responseSnippet = snippet.slice(0, 300);
|
|
562
|
-
}
|
|
563
|
-
return meta;
|
|
564
|
-
}
|
|
565
|
-
function looksLikeHtml(text) {
|
|
566
|
-
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
567
|
-
}
|
|
568
|
-
function compactMessage(message) {
|
|
569
|
-
const compact = message.replace(/\s+/g, " ").trim();
|
|
570
|
-
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
618
|
// src/sql/SQLMigrations.ts
|
|
574
619
|
var SQLMigrations = class {
|
|
575
620
|
constructor(service, dbName) {
|
|
@@ -933,18 +978,19 @@ var SQLService = class extends BaseService {
|
|
|
933
978
|
}
|
|
934
979
|
// === Private helpers ===
|
|
935
980
|
async invokeSQL(dbName, actions, body, signal) {
|
|
936
|
-
const session = this.context.session;
|
|
937
981
|
const actionList = Array.isArray(actions) ? actions : [actions];
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
"Content-Type": "application/json"
|
|
982
|
+
return this.invokeWithRetry(
|
|
983
|
+
`${this.host}/invoke`,
|
|
984
|
+
() => {
|
|
985
|
+
const session = this.context.session;
|
|
986
|
+
return actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
|
|
944
987
|
},
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
988
|
+
{
|
|
989
|
+
headers: { "Content-Type": "application/json" },
|
|
990
|
+
body: JSON.stringify(body),
|
|
991
|
+
signal
|
|
992
|
+
}
|
|
993
|
+
);
|
|
948
994
|
}
|
|
949
995
|
actionForSql(sql, fallback) {
|
|
950
996
|
const token = firstSqlToken(sql);
|
|
@@ -985,6 +1031,8 @@ var SQLService = class extends BaseService {
|
|
|
985
1031
|
if (response.status === 413 && parseStorageRejection(errorText)) {
|
|
986
1032
|
return err(storageRejectionError("sql", 413, meta, errorText));
|
|
987
1033
|
}
|
|
1034
|
+
const retryError = nodeRetryError("sql", `SQL ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
|
|
1035
|
+
if (retryError) return err(retryError);
|
|
988
1036
|
const errorBody = parseServiceErrorBody(errorText);
|
|
989
1037
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
990
1038
|
response.status,
|