@mdbase-dev/connect 0.1.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +696 -0
  3. package/dist/advanced.d.ts +9 -0
  4. package/dist/advanced.js +6 -0
  5. package/dist/application-contract.d.ts +22 -0
  6. package/dist/application-contract.js +75 -0
  7. package/dist/application-identity.d.ts +34 -0
  8. package/dist/application-identity.js +148 -0
  9. package/dist/application-session.d.ts +167 -0
  10. package/dist/application-session.js +722 -0
  11. package/dist/async.d.ts +1 -0
  12. package/dist/async.js +19 -0
  13. package/dist/authority-proof.d.ts +3 -0
  14. package/dist/authority-proof.js +24 -0
  15. package/dist/authority-sync-request.d.ts +3 -0
  16. package/dist/authority-sync-request.js +20 -0
  17. package/dist/authorization-refresh.d.ts +14 -0
  18. package/dist/authorization-refresh.js +41 -0
  19. package/dist/authorization-types.d.ts +7 -0
  20. package/dist/authorization-types.js +1 -0
  21. package/dist/authorization-url.d.ts +5 -0
  22. package/dist/authorization-url.js +34 -0
  23. package/dist/base64.d.ts +3 -0
  24. package/dist/base64.js +21 -0
  25. package/dist/browser/integrity.json +4 -0
  26. package/dist/browser/mdbase-connect.min.js +3 -0
  27. package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
  28. package/dist/browser.d.ts +3 -0
  29. package/dist/browser.js +5 -0
  30. package/dist/capabilities.d.ts +24 -0
  31. package/dist/capabilities.js +91 -0
  32. package/dist/collection-client.d.ts +66 -0
  33. package/dist/collection-client.js +686 -0
  34. package/dist/connect-authorization-helpers.d.ts +4 -0
  35. package/dist/connect-authorization-helpers.js +25 -0
  36. package/dist/connect-options.d.ts +35 -0
  37. package/dist/connect-options.js +1 -0
  38. package/dist/connect.d.ts +66 -0
  39. package/dist/connect.js +711 -0
  40. package/dist/connection-file-transport.d.ts +34 -0
  41. package/dist/connection-file-transport.js +85 -0
  42. package/dist/connection-index.d.ts +3 -0
  43. package/dist/connection-index.js +20 -0
  44. package/dist/connection-notifications.d.ts +31 -0
  45. package/dist/connection-notifications.js +197 -0
  46. package/dist/connection-transport-internals.d.ts +24 -0
  47. package/dist/connection-transport-internals.js +1 -0
  48. package/dist/connection-transport.d.ts +64 -0
  49. package/dist/connection-transport.js +725 -0
  50. package/dist/connection-types.d.ts +38 -0
  51. package/dist/connection-types.js +1 -0
  52. package/dist/connection.d.ts +175 -0
  53. package/dist/connection.js +606 -0
  54. package/dist/crypto-entry.d.ts +7 -0
  55. package/dist/crypto-entry.js +4 -0
  56. package/dist/crypto.d.ts +63 -0
  57. package/dist/crypto.js +409 -0
  58. package/dist/direct-access.d.ts +3 -0
  59. package/dist/direct-access.js +29 -0
  60. package/dist/errors.d.ts +45 -0
  61. package/dist/errors.js +142 -0
  62. package/dist/external-store.d.ts +10 -0
  63. package/dist/external-store.js +10 -0
  64. package/dist/file-crypto.d.ts +25 -0
  65. package/dist/file-crypto.js +132 -0
  66. package/dist/file-sha256.d.ts +12 -0
  67. package/dist/file-sha256.js +143 -0
  68. package/dist/file-stream-source.d.ts +17 -0
  69. package/dist/file-stream-source.js +157 -0
  70. package/dist/file-transfer-internals.d.ts +23 -0
  71. package/dist/file-transfer-internals.js +228 -0
  72. package/dist/files.d.ts +115 -0
  73. package/dist/files.js +563 -0
  74. package/dist/fresh-encrypted-request.d.ts +4 -0
  75. package/dist/fresh-encrypted-request.js +28 -0
  76. package/dist/grant-key-leases.d.ts +24 -0
  77. package/dist/grant-key-leases.js +144 -0
  78. package/dist/hosted-file-request.d.ts +6 -0
  79. package/dist/hosted-file-request.js +63 -0
  80. package/dist/hosted-operation-response.d.ts +5 -0
  81. package/dist/hosted-operation-response.js +26 -0
  82. package/dist/index.d.ts +27 -0
  83. package/dist/index.js +10 -0
  84. package/dist/internal-types.d.ts +100 -0
  85. package/dist/internal-types.js +1 -0
  86. package/dist/local-file-transport.d.ts +37 -0
  87. package/dist/local-file-transport.js +457 -0
  88. package/dist/mdbase-connect.d.ts +28 -0
  89. package/dist/mdbase-connect.js +53 -0
  90. package/dist/notifications.d.ts +48 -0
  91. package/dist/notifications.js +52 -0
  92. package/dist/operation-helpers.d.ts +27 -0
  93. package/dist/operation-helpers.js +144 -0
  94. package/dist/operation-types.d.ts +707 -0
  95. package/dist/operation-types.js +1 -0
  96. package/dist/outcomes.d.ts +51 -0
  97. package/dist/outcomes.js +194 -0
  98. package/dist/pending-mutation-store.d.ts +13 -0
  99. package/dist/pending-mutation-store.js +64 -0
  100. package/dist/pending-recovery-transports.d.ts +5 -0
  101. package/dist/pending-recovery-transports.js +52 -0
  102. package/dist/popup-authorization.d.ts +31 -0
  103. package/dist/popup-authorization.js +100 -0
  104. package/dist/query-pagination.d.ts +6 -0
  105. package/dist/query-pagination.js +119 -0
  106. package/dist/request-budget.d.ts +34 -0
  107. package/dist/request-budget.js +104 -0
  108. package/dist/request-coordinator.d.ts +29 -0
  109. package/dist/request-coordinator.js +213 -0
  110. package/dist/retired-grant-keys.d.ts +9 -0
  111. package/dist/retired-grant-keys.js +56 -0
  112. package/dist/runtime-utils.d.ts +52 -0
  113. package/dist/runtime-utils.js +368 -0
  114. package/dist/saved-view-pagination.d.ts +6 -0
  115. package/dist/saved-view-pagination.js +76 -0
  116. package/dist/selection.d.ts +48 -0
  117. package/dist/selection.js +112 -0
  118. package/dist/session.d.ts +114 -0
  119. package/dist/session.js +569 -0
  120. package/dist/shared-operation.d.ts +7 -0
  121. package/dist/shared-operation.js +41 -0
  122. package/dist/stored-token.d.ts +10 -0
  123. package/dist/stored-token.js +64 -0
  124. package/dist/structured-readiness.d.ts +77 -0
  125. package/dist/structured-readiness.js +126 -0
  126. package/dist/token-response.d.ts +12 -0
  127. package/dist/token-response.js +81 -0
  128. package/dist/transient-retry.d.ts +22 -0
  129. package/dist/transient-retry.js +59 -0
  130. package/dist/watch-policy.d.ts +8 -0
  131. package/dist/watch-policy.js +12 -0
  132. package/package.json +51 -0
@@ -0,0 +1,41 @@
1
+ import { requestAbortReason, withRequestBudget } from "./request-budget.js";
2
+ export class SharedOperationMap {
3
+ operations = new Map();
4
+ run(key, options, defaultTimeoutMs, start) {
5
+ if (options?.signal?.aborted)
6
+ return Promise.reject(requestAbortReason(options.signal));
7
+ const operation = this.operations.get(key) ?? this.begin(key, start);
8
+ return this.wait(key, operation, options, defaultTimeoutMs);
9
+ }
10
+ begin(key, start) {
11
+ const controller = new AbortController();
12
+ const operation = {
13
+ controller,
14
+ waiters: 0,
15
+ settled: false,
16
+ promise: undefined
17
+ };
18
+ operation.promise = Promise.resolve()
19
+ .then(() => start(controller.signal))
20
+ .finally(() => {
21
+ operation.settled = true;
22
+ if (this.operations.get(key) === operation)
23
+ this.operations.delete(key);
24
+ });
25
+ this.operations.set(key, operation);
26
+ return operation;
27
+ }
28
+ async wait(key, operation, options, defaultTimeoutMs) {
29
+ operation.waiters += 1;
30
+ try {
31
+ return await withRequestBudget(options, defaultTimeoutMs, () => operation.promise);
32
+ }
33
+ finally {
34
+ operation.waiters -= 1;
35
+ if (operation.waiters === 0 && !operation.settled && this.operations.get(key) === operation) {
36
+ this.operations.delete(key);
37
+ operation.controller.abort();
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,10 @@
1
+ import type { StoredToken } from "./internal-types.js";
2
+ interface ReadStoredTokenOptions {
3
+ stored: string | null;
4
+ collectionId: string;
5
+ relayEncryption: "required" | "disabled";
6
+ invalidate(keyHandle?: string, reason?: "invalid_stored_grant" | "legacy_scope_reauthorization_required"): void;
7
+ directCapable(token: StoredToken): boolean;
8
+ }
9
+ export declare function readStoredToken({ stored, collectionId, relayEncryption, invalidate, directCapable }: ReadStoredTokenOptions): StoredToken | null;
10
+ export {};
@@ -0,0 +1,64 @@
1
+ import { isCanonicalGrantScope, parseGrantScope, parseStored, validFileCapability, validStoredAuthority, validStoredEncryption } from "./runtime-utils.js";
2
+ export function readStoredToken({ stored, collectionId, relayEncryption, invalidate, directCapable }) {
3
+ const token = parseStored(stored);
4
+ const reject = (keyHandle, reason = "invalid_stored_grant") => {
5
+ invalidate(typeof keyHandle === "string" ? keyHandle : undefined, reason);
6
+ return null;
7
+ };
8
+ if (!token) {
9
+ if (stored)
10
+ reject();
11
+ return null;
12
+ }
13
+ if (token.version !== 1
14
+ || typeof token.accessToken !== "string"
15
+ || token.accessToken.length === 0
16
+ || typeof token.clientId !== "string"
17
+ || token.clientId.length === 0
18
+ || token.collectionId !== collectionId
19
+ || typeof token.collectionName !== "string"
20
+ || token.collectionName.length === 0
21
+ || !Array.isArray(token.operations)
22
+ || token.operations.some((operation) => typeof operation !== "string")
23
+ || typeof token.expiresAt !== "number"
24
+ || !Number.isFinite(token.expiresAt)
25
+ || (token.refreshToken !== undefined
26
+ && (typeof token.refreshToken !== "string" || token.refreshToken.length === 0))
27
+ || (token.refreshExpiresAt !== undefined
28
+ && (typeof token.refreshExpiresAt !== "number" || !Number.isFinite(token.refreshExpiresAt))))
29
+ return reject(token.keyHandle);
30
+ const scope = parseGrantScope(token.scope);
31
+ if (!scope)
32
+ return reject(token.keyHandle);
33
+ if (!isCanonicalGrantScope(scope)) {
34
+ return reject(token.keyHandle, "legacy_scope_reauthorization_required");
35
+ }
36
+ if (token.fileCapability && !validFileCapability(token.fileCapability)) {
37
+ return reject(token.keyHandle);
38
+ }
39
+ if (token.authority && !validStoredAuthority(token.authority, token.collectionId)) {
40
+ return reject(token.keyHandle);
41
+ }
42
+ if (relayEncryption === "required") {
43
+ if (token.authority) {
44
+ if (!token.keyHandle || !token.authority.proofPublicKey) {
45
+ return reject(token.keyHandle);
46
+ }
47
+ }
48
+ else if (!token.grantId
49
+ || !token.keyHandle
50
+ || !validStoredEncryption(token.encryption, token.collectionId)) {
51
+ return reject(token.keyHandle);
52
+ }
53
+ }
54
+ if (token.expiresAt <= Date.now()
55
+ && (!token.refreshToken || (token.refreshExpiresAt ?? 0) <= Date.now())) {
56
+ // The cloud bearer and local grant proof have separate lifetimes. Keep an
57
+ // encrypted local grant usable while the connector still recognizes it.
58
+ if (directCapable(token))
59
+ return token;
60
+ invalidate(token.keyHandle);
61
+ return null;
62
+ }
63
+ return token;
64
+ }
@@ -0,0 +1,77 @@
1
+ import type { ContractRequirement, FileAction, JsonObject } from "@mdbase-dev/connect-protocol";
2
+ import type { MdbaseApplicationManifest } from "./application-contract.js";
3
+ import type { MdbaseConnectionInfo } from "./connection-types.js";
4
+ import { type MdbaseEffectiveCapabilities, type MdbaseCapabilityEvidence } from "./capabilities.js";
5
+ import type { MdbaseConnection } from "./connection.js";
6
+ import type { ConnectRequestOptions } from "./operation-types.js";
7
+ type ReadinessPublication = {
8
+ status: "authorization_required";
9
+ } | {
10
+ status: "blocked";
11
+ problem: {
12
+ code: string;
13
+ message: string;
14
+ };
15
+ };
16
+ /** Live verification owns no session state; callers supply identity and cancellation guards. */
17
+ export declare function verifyStructuredReadiness<Frontmatter extends JsonObject>(input: {
18
+ collectionId: string;
19
+ readiness: MdbaseStructuredReadiness;
20
+ contracts: ContractRequirement[];
21
+ connection: MdbaseConnection<Frontmatter>;
22
+ controller: AbortController;
23
+ options?: ConnectRequestOptions;
24
+ requestMs: number | null;
25
+ isCurrent: () => boolean;
26
+ publish: (result: ReadinessPublication) => void;
27
+ }): Promise<boolean>;
28
+ export declare function publishedStructuredReadiness(source: MdbaseStructuredReadiness, status: string, verification?: "cached" | "verified"): MdbaseStructuredReadiness;
29
+ export interface ApplicationSessionContext {
30
+ collectionId: string;
31
+ info: MdbaseConnectionInfo;
32
+ capabilities: MdbaseEffectiveCapabilities;
33
+ /** Present only for semantic contract v2. */
34
+ readiness?: MdbaseStructuredReadiness;
35
+ connections: MdbaseConnectionInfo[];
36
+ }
37
+ export declare function applicationReadinessContext(manifest: MdbaseApplicationManifest, collectionId: string, info: MdbaseConnectionInfo, connections: MdbaseConnectionInfo[]): ApplicationSessionContext;
38
+ export declare function verificationKey(manifest: MdbaseApplicationManifest, collectionId: string): string;
39
+ export declare function verificationValue(manifest: MdbaseApplicationManifest): string;
40
+ export declare function collectionSetupInput(manifest: MdbaseApplicationManifest, applicationId: string, declarationDigest: string, typePackAdoptions?: Record<string, Record<string, string>>): {
41
+ applicationId: string;
42
+ declarationDigest: string;
43
+ requirements: {
44
+ configuration: import("@mdbase-dev/connect-protocol").ConfigurationRequirement[];
45
+ };
46
+ provisions: {
47
+ configuration: import("@mdbase-dev/connect-protocol").ConfigurationProvision[];
48
+ typePacks: import("@mdbase-dev/connect-protocol").TypePackProvision[];
49
+ };
50
+ typePackAdoptions?: Record<string, Record<string, string>> | undefined;
51
+ };
52
+ /** Independent v2 declarations; never legacy capability aliases. */
53
+ export interface MdbaseStructuredReadiness {
54
+ files: {
55
+ state: "not_required" | "available" | "requires_authorization";
56
+ missingRequiredActions: FileAction[];
57
+ missingOptionalActions: FileAction[];
58
+ scopeMatches: boolean;
59
+ evidence: MdbaseCapabilityEvidence[];
60
+ };
61
+ contracts: {
62
+ state: "not_required" | "checking" | "verified" | "requires_setup" | "requires_authorization" | "temporarily_unavailable";
63
+ missing: ContractRequirement[];
64
+ evidence: MdbaseCapabilityEvidence[];
65
+ };
66
+ setup: {
67
+ state: "not_required" | "checking" | "review_required" | "current" | "cached" | "blocked" | "requires_authorization";
68
+ evidence: MdbaseCapabilityEvidence[];
69
+ };
70
+ notifications: {
71
+ state: "not_required" | "requires_registration";
72
+ registration: "unverified";
73
+ evidence: MdbaseCapabilityEvidence[];
74
+ };
75
+ }
76
+ export declare function structuredReadiness(manifest: MdbaseApplicationManifest, info: MdbaseConnectionInfo): MdbaseStructuredReadiness;
77
+ export {};
@@ -0,0 +1,126 @@
1
+ /* The versioned protocol package is the sole capability-to-operation compiler. */
2
+ import { effectiveCapabilities } from "./capabilities.js";
3
+ import { MdbaseConnectError } from "./errors.js";
4
+ import { withRequestBudget } from "./request-budget.js";
5
+ /** Live verification owns no session state; callers supply identity and cancellation guards. */
6
+ export async function verifyStructuredReadiness(input) {
7
+ const { collectionId, readiness, contracts, connection, controller, options, requestMs, isCurrent, publish } = input;
8
+ const abort = () => controller.abort(options?.signal?.reason);
9
+ if (options?.signal?.aborted)
10
+ abort();
11
+ else
12
+ options?.signal?.addEventListener("abort", abort, { once: true });
13
+ try {
14
+ const outcome = await withRequestBudget({ ...options, signal: controller.signal }, requestMs, request => connection.describe(request));
15
+ if (!isCurrent())
16
+ return false;
17
+ if (!outcome.ok) {
18
+ const authorizationRequired = ["access_denied", "collection_access_denied", "insufficient_access", "not_authorized", "authorization_expired", "application_declaration_mismatch"].includes(outcome.problem.code);
19
+ readiness.contracts = { state: authorizationRequired ? "requires_authorization" : "temporarily_unavailable", missing: [], evidence: [{ source: "runtime", fact: outcome.problem.message }] };
20
+ publish(authorizationRequired
21
+ ? { status: "authorization_required" }
22
+ : { status: "blocked", problem: outcome.problem });
23
+ return false;
24
+ }
25
+ if (outcome.value.collectionId !== collectionId) {
26
+ readiness.contracts = { state: "temporarily_unavailable", missing: [], evidence: [] };
27
+ publish({ status: "blocked", problem: { code: "collection_not_ready", message: "Description belongs to a different collection." } });
28
+ return false;
29
+ }
30
+ const missing = contracts.filter(required => !outcome.value.contracts.some(actual => actual.id === required.id && actual.version === required.version && actual.digest === required.digest));
31
+ readiness.contracts = { state: missing.length ? "requires_setup" : "verified", missing,
32
+ evidence: [{ source: "authority", fact: "Exact contract ID, version and digest checked by live describe for this collection." }] };
33
+ if (missing.length) {
34
+ publish({ status: "blocked", problem: { code: "collection_not_ready", message: "The collection is missing required exact contracts." } });
35
+ return false;
36
+ }
37
+ }
38
+ catch (error) {
39
+ if (!isCurrent())
40
+ return false;
41
+ if (!(error instanceof MdbaseConnectError))
42
+ throw error;
43
+ readiness.contracts = { state: "temporarily_unavailable", missing: [], evidence: [{ source: "runtime", fact: error.problem.message }] };
44
+ publish({ status: "blocked", problem: error.problem });
45
+ return false;
46
+ }
47
+ finally {
48
+ options?.signal?.removeEventListener("abort", abort);
49
+ }
50
+ return true;
51
+ }
52
+ export function publishedStructuredReadiness(source, status, verification) {
53
+ const readiness = structuredClone(source);
54
+ if (readiness.setup.state !== "not_required" && readiness.setup.state !== "current") {
55
+ readiness.setup.state = status === "ready" ? (verification === "cached" ? "cached" : "current")
56
+ : status === "setup_review_required" ? "review_required"
57
+ : status === "authorization_required" ? "requires_authorization"
58
+ : status === "blocked" ? "blocked" : "checking";
59
+ readiness.setup.evidence = [{ source: status === "ready" && verification === "cached" ? "runtime" : "authority",
60
+ fact: `Collection setup verification: ${readiness.setup.state}.` }];
61
+ }
62
+ return readiness;
63
+ }
64
+ export function applicationReadinessContext(manifest, collectionId, info, connections) {
65
+ return {
66
+ collectionId, info,
67
+ capabilities: effectiveCapabilities(manifest.requirements.capabilities, manifest, info),
68
+ ...(manifest.requirements?.capabilities?.contract_version === 2
69
+ ? { readiness: structuredReadiness(manifest, info) } : {}),
70
+ connections
71
+ };
72
+ }
73
+ export function verificationKey(manifest, collectionId) {
74
+ return `mdbase-application-session:v1:${manifest.id}:${collectionId}`;
75
+ }
76
+ export function verificationValue(manifest) {
77
+ return JSON.stringify({
78
+ capabilities: manifest.requirements?.capabilities,
79
+ requirements: manifest.requirements?.configuration ?? [],
80
+ provisions: manifest.provisions ?? {}
81
+ });
82
+ }
83
+ export function collectionSetupInput(manifest, applicationId, declarationDigest, typePackAdoptions) {
84
+ return {
85
+ applicationId, declarationDigest,
86
+ requirements: { configuration: manifest.requirements?.configuration ?? [] },
87
+ provisions: {
88
+ configuration: manifest.provisions?.configuration ?? [],
89
+ typePacks: manifest.provisions?.type_packs ?? []
90
+ },
91
+ ...(typePackAdoptions ? { typePackAdoptions } : {})
92
+ };
93
+ }
94
+ function sameScope(left, right) {
95
+ if (!right || left.kind !== right.kind)
96
+ return false;
97
+ if (left.kind === "collection" || right.kind === "collection")
98
+ return true;
99
+ const a = [...new Set(left.folders)].sort();
100
+ const b = [...new Set(right.folders)].sort();
101
+ return JSON.stringify(a) === JSON.stringify(b);
102
+ }
103
+ export function structuredReadiness(manifest, info) {
104
+ const files = manifest.requirements?.files;
105
+ const requirement = files && "required" in files ? files : undefined;
106
+ const scopeMatches = !requirement || sameScope(requirement.scope, info.fileCapability?.scope);
107
+ const missing = (actions) => actions.filter(action => !scopeMatches || !info.fileCapability?.actions.includes(action));
108
+ const missingRequiredActions = missing(requirement?.required ?? []);
109
+ const missingOptionalActions = missing(requirement?.optional ?? []);
110
+ const contracts = manifest.requirements?.contracts ?? [];
111
+ const setup = (manifest.provisions?.type_packs.length ?? 0) > 0 || (manifest.provisions?.configuration?.length ?? 0) > 0;
112
+ return {
113
+ files: {
114
+ state: !requirement ? "not_required" : missingRequiredActions.length ? "requires_authorization" : "available",
115
+ missingRequiredActions, missingOptionalActions, scopeMatches,
116
+ evidence: [{ source: "authorization", fact: "Compared exact file actions and folder scope with connection.info().fileCapability." }]
117
+ },
118
+ contracts: { state: contracts.length ? "checking" : "not_required", missing: [], evidence: [] },
119
+ setup: { state: setup ? "checking" : "not_required", evidence: [] },
120
+ notifications: {
121
+ state: manifest.notifications?.criteria.length ? "requires_registration" : "not_required",
122
+ registration: "unverified",
123
+ evidence: [{ source: "runtime", fact: "The connection API exposes registration commands but no current delivery-registration query. Manifest criteria do not prove approval or delivery registration." }]
124
+ }
125
+ };
126
+ }
@@ -0,0 +1,12 @@
1
+ import type { StoredToken } from "./internal-types.js";
2
+ interface StoredTokenResponseOptions {
3
+ body: any;
4
+ clientId: string;
5
+ keyHandle?: string;
6
+ previous: StoredToken | null;
7
+ retiredKeyHandles?: string[];
8
+ defaultApplicationOrigin: string;
9
+ pinConnectorIdentity(connectorId: string, publicKey: string): void;
10
+ }
11
+ export declare function storedTokenFromResponse({ body, clientId, keyHandle, previous, retiredKeyHandles, defaultApplicationOrigin, pinConnectorIdentity }: StoredTokenResponseOptions): StoredToken;
12
+ export {};
@@ -0,0 +1,81 @@
1
+ import { validateGrantEncryption } from "./crypto.js";
2
+ import { connectError } from "./errors.js";
3
+ import { isCanonicalGrantScope, parseGrantScope, validAuthorityTokenResponse, validFileCapability } from "./runtime-utils.js";
4
+ export function storedTokenFromResponse({ body, clientId, keyHandle, previous, retiredKeyHandles, defaultApplicationOrigin, pinConnectorIdentity }) {
5
+ const collectionId = body.collection_id;
6
+ if (typeof collectionId !== "string") {
7
+ throw connectError("invalid_token_response", "Authorization returned no collection ID.");
8
+ }
9
+ const scope = parseGrantScope(body.scope);
10
+ if (!scope) {
11
+ throw connectError("invalid_token_response", "Authorization returned no valid collection scope.");
12
+ }
13
+ if (!isCanonicalGrantScope(scope)) {
14
+ throw connectError("legacy_scope_reauthorization_required", "Authorization returned a legacy contract-scoped grant. Reauthorize for the entire collection.");
15
+ }
16
+ if (body.authority && !validAuthorityTokenResponse(body.authority, collectionId)) {
17
+ throw connectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
18
+ }
19
+ if (body.authority && body.encryption) {
20
+ throw connectError("invalid_token_response", "Authorization returned conflicting collection transports.");
21
+ }
22
+ if (body.file_capability !== null && body.file_capability !== undefined
23
+ && !validFileCapability(body.file_capability)) {
24
+ throw connectError("invalid_token_response", "Authorization returned an invalid file capability.");
25
+ }
26
+ if (body.encryption) {
27
+ try {
28
+ validateGrantEncryption(body.encryption);
29
+ }
30
+ catch {
31
+ throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
32
+ }
33
+ pinConnectorIdentity(body.encryption.connector_id, body.encryption.connector_agreement_public_key);
34
+ if (body.encryption.collection_id !== collectionId
35
+ || typeof body.grant_id !== "string"
36
+ || body.grant_id.length === 0) {
37
+ throw connectError("invalid_token_response", "Authorization returned an encrypted relay binding for another grant.");
38
+ }
39
+ }
40
+ if (previous?.encryption
41
+ && body.encryption
42
+ && previous.keyHandle
43
+ && previous.keyHandle === keyHandle
44
+ && (previous.grantId !== body.grant_id
45
+ || previous.encryption.connector_id !== body.encryption.connector_id
46
+ || previous.encryption.connector_agreement_public_key
47
+ !== body.encryption.connector_agreement_public_key
48
+ || previous.encryption.application_agreement_public_key
49
+ !== body.encryption.application_agreement_public_key)) {
50
+ throw connectError("connector_identity_changed", "The connector identity changed during authorization renewal. Reauthorize before sending collection data.");
51
+ }
52
+ return {
53
+ version: 1,
54
+ accessToken: body.access_token,
55
+ refreshToken: body.refresh_token,
56
+ clientId,
57
+ collectionId,
58
+ collectionName: body.collection_name ?? `Collection ${collectionId.slice(0, 8)}`,
59
+ operations: body.operations,
60
+ scope,
61
+ expiresAt: Date.now() + body.expires_in * 1_000,
62
+ refreshExpiresAt: body.refresh_expires_in
63
+ ? Date.now() + body.refresh_expires_in * 1_000
64
+ : undefined,
65
+ grantId: body.grant_id,
66
+ encryption: body.encryption ?? undefined,
67
+ fileCapability: body.file_capability ?? undefined,
68
+ applicationOrigin: body.application_origin ?? defaultApplicationOrigin,
69
+ keyHandle,
70
+ ...(retiredKeyHandles?.length ? { retiredKeyHandles } : {}),
71
+ savedAt: Date.now(),
72
+ authority: body.authority ? {
73
+ operationsUrl: body.authority.operations_url,
74
+ syncUrl: body.authority.sync_url,
75
+ filesUrl: body.authority.files_url,
76
+ replicaId: body.authority.replica_id,
77
+ accessToken: body.authority.access_token,
78
+ proofPublicKey: body.authority.proof_public_key
79
+ } : undefined
80
+ };
81
+ }
@@ -0,0 +1,22 @@
1
+ import { MdbaseConnectError } from "./errors.js";
2
+ import type { OperationAttempt, PendingMutation } from "./internal-types.js";
3
+ export declare const MAX_CONNECTOR_BUSY_RETRIES = 8;
4
+ export declare const MAX_TRANSIENT_CHUNK_ATTEMPTS = 10;
5
+ export declare function isConnectorBusy(error: unknown): error is MdbaseConnectError;
6
+ export declare function retryExplicitConnectorBusy<Result>(options: {
7
+ error: unknown;
8
+ attempt: OperationAttempt;
9
+ completedRetries: number;
10
+ signal?: AbortSignal;
11
+ knownRejectedMutationRetry: boolean;
12
+ clearPending(requestId: string): void;
13
+ retry(pending: PendingMutation | undefined, knownRejected: boolean): Promise<Result>;
14
+ }): Promise<{
15
+ retried: false;
16
+ } | {
17
+ retried: true;
18
+ result: Result;
19
+ }>;
20
+ export declare function isExplicitConnectorBusyResponse(response: Response): Promise<boolean>;
21
+ export declare function waitForConnectorAvailability(retryIndex: number, signal?: AbortSignal): Promise<void>;
22
+ export declare function waitForTransientChunkRetry(completedAttempt: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { abortableDelay } from "./async.js";
2
+ import { MdbaseConnectError, connectError } from "./errors.js";
3
+ export const MAX_CONNECTOR_BUSY_RETRIES = 8;
4
+ export const MAX_TRANSIENT_CHUNK_ATTEMPTS = 10;
5
+ export function isConnectorBusy(error) {
6
+ return error instanceof MdbaseConnectError && error.code === "connector_busy";
7
+ }
8
+ export async function retryExplicitConnectorBusy(options) {
9
+ if (!isConnectorBusy(options.error)
10
+ || options.completedRetries >= MAX_CONNECTOR_BUSY_RETRIES) {
11
+ return { retried: false };
12
+ }
13
+ try {
14
+ await waitForConnectorAvailability(options.completedRetries, options.signal);
15
+ }
16
+ catch (error) {
17
+ if (options.attempt.pendingMutation
18
+ && (options.knownRejectedMutationRetry || !options.attempt.resumingMutation)) {
19
+ options.clearPending(options.attempt.requestId);
20
+ }
21
+ throw error;
22
+ }
23
+ return {
24
+ retried: true,
25
+ result: await options.retry(options.attempt.pendingMutationRecord, options.knownRejectedMutationRetry
26
+ || (options.attempt.pendingMutation === true && !options.attempt.resumingMutation))
27
+ };
28
+ }
29
+ export async function isExplicitConnectorBusyResponse(response) {
30
+ if (response.status !== 502 && response.status !== 503)
31
+ return false;
32
+ const body = await response.clone().json().catch(() => null);
33
+ return body?.error === "connector_busy"
34
+ || (typeof body?.error === "object"
35
+ && body.error?.code === "connector_busy");
36
+ }
37
+ export function waitForConnectorAvailability(retryIndex, signal) {
38
+ return retryDelay(jitteredBackoff(50, 800, retryIndex), signal);
39
+ }
40
+ export function waitForTransientChunkRetry(completedAttempt, signal) {
41
+ return retryDelay(jitteredBackoff(100, 1_000, completedAttempt - 1), signal);
42
+ }
43
+ async function retryDelay(milliseconds, signal) {
44
+ try {
45
+ await abortableDelay(milliseconds, signal);
46
+ }
47
+ catch (cause) {
48
+ if (signal?.aborted) {
49
+ if (signal.reason instanceof MdbaseConnectError)
50
+ throw signal.reason;
51
+ throw connectError("operation_cancelled", "The operation was cancelled while waiting for connector capacity.", { operationOutcome: "not_sent", cause });
52
+ }
53
+ throw cause;
54
+ }
55
+ }
56
+ function jitteredBackoff(base, cap, exponent) {
57
+ const bounded = Math.min(cap, base * (2 ** Math.max(0, exponent)));
58
+ return Math.max(1, Math.round(bounded * (0.75 + Math.random() * 0.5)));
59
+ }
@@ -0,0 +1,8 @@
1
+ import type { WatchOptions } from "./operation-types.js";
2
+ export interface ResolvedWatchRetryOptions {
3
+ initialDelayMs: number;
4
+ maxDelayMs: number;
5
+ multiplier: number;
6
+ maxAttempts?: number;
7
+ }
8
+ export declare function watchRetryPolicy(options: WatchOptions["retry"]): ResolvedWatchRetryOptions | undefined;
@@ -0,0 +1,12 @@
1
+ export function watchRetryPolicy(options) {
2
+ if (options === false)
3
+ return undefined;
4
+ return {
5
+ initialDelayMs: Math.max(0, options?.initialDelayMs ?? 500),
6
+ maxDelayMs: Math.max(0, options?.maxDelayMs ?? 15_000),
7
+ multiplier: Math.max(1, options?.multiplier ?? 2),
8
+ ...(options?.maxAttempts === undefined
9
+ ? {}
10
+ : { maxAttempts: Math.max(0, options.maxAttempts) })
11
+ };
12
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mdbase-dev/connect",
3
+ "version": "0.1.0-beta.100",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mdbase-dev/mdbase-connect.git",
10
+ "directory": "packages/client"
11
+ },
12
+ "homepage": "https://mdbase.dev/sdk/",
13
+ "sideEffects": false,
14
+ "browser": "./dist/browser/mdbase-connect.min.js",
15
+ "unpkg": "./dist/browser/mdbase-connect.min.js",
16
+ "jsdelivr": "./dist/browser/mdbase-connect.min.js",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./advanced": {
23
+ "types": "./dist/advanced.d.ts",
24
+ "import": "./dist/advanced.js"
25
+ },
26
+ "./crypto": {
27
+ "types": "./dist/crypto-entry.d.ts",
28
+ "import": "./dist/crypto-entry.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "dependencies": {
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.100"
37
+ },
38
+ "devDependencies": {
39
+ "esbuild": "^0.28.1",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^4.1.10"
42
+ },
43
+ "scripts": {
44
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/build-browser.mjs",
45
+ "test": "vitest run",
46
+ "test:browser-storage": "pnpm build && node scripts/browser-storage.test.mjs",
47
+ "test:consumer-spikes": "tsc -p tsconfig.consumer-spikes.json",
48
+ "test:public-api": "pnpm build && pnpm --filter @mdbase-dev/connect-protocol build && pnpm --filter @mdbase-dev/connect-testing build && tsc -p tsconfig.public-api.json && node scripts/check-public-api.mjs && node scripts/test-packed-api.mjs",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit"
50
+ }
51
+ }