@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.
- package/LICENSE +21 -0
- package/README.md +696 -0
- package/dist/advanced.d.ts +9 -0
- package/dist/advanced.js +6 -0
- package/dist/application-contract.d.ts +22 -0
- package/dist/application-contract.js +75 -0
- package/dist/application-identity.d.ts +34 -0
- package/dist/application-identity.js +148 -0
- package/dist/application-session.d.ts +167 -0
- package/dist/application-session.js +722 -0
- package/dist/async.d.ts +1 -0
- package/dist/async.js +19 -0
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/authority-sync-request.d.ts +3 -0
- package/dist/authority-sync-request.js +20 -0
- package/dist/authorization-refresh.d.ts +14 -0
- package/dist/authorization-refresh.js +41 -0
- package/dist/authorization-types.d.ts +7 -0
- package/dist/authorization-types.js +1 -0
- package/dist/authorization-url.d.ts +5 -0
- package/dist/authorization-url.js +34 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.js +21 -0
- package/dist/browser/integrity.json +4 -0
- package/dist/browser/mdbase-connect.min.js +3 -0
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.d.ts +24 -0
- package/dist/capabilities.js +91 -0
- package/dist/collection-client.d.ts +66 -0
- package/dist/collection-client.js +686 -0
- package/dist/connect-authorization-helpers.d.ts +4 -0
- package/dist/connect-authorization-helpers.js +25 -0
- package/dist/connect-options.d.ts +35 -0
- package/dist/connect-options.js +1 -0
- package/dist/connect.d.ts +66 -0
- package/dist/connect.js +711 -0
- package/dist/connection-file-transport.d.ts +34 -0
- package/dist/connection-file-transport.js +85 -0
- package/dist/connection-index.d.ts +3 -0
- package/dist/connection-index.js +20 -0
- package/dist/connection-notifications.d.ts +31 -0
- package/dist/connection-notifications.js +197 -0
- package/dist/connection-transport-internals.d.ts +24 -0
- package/dist/connection-transport-internals.js +1 -0
- package/dist/connection-transport.d.ts +64 -0
- package/dist/connection-transport.js +725 -0
- package/dist/connection-types.d.ts +38 -0
- package/dist/connection-types.js +1 -0
- package/dist/connection.d.ts +175 -0
- package/dist/connection.js +606 -0
- package/dist/crypto-entry.d.ts +7 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.d.ts +63 -0
- package/dist/crypto.js +409 -0
- package/dist/direct-access.d.ts +3 -0
- package/dist/direct-access.js +29 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +142 -0
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-crypto.d.ts +25 -0
- package/dist/file-crypto.js +132 -0
- package/dist/file-sha256.d.ts +12 -0
- package/dist/file-sha256.js +143 -0
- package/dist/file-stream-source.d.ts +17 -0
- package/dist/file-stream-source.js +157 -0
- package/dist/file-transfer-internals.d.ts +23 -0
- package/dist/file-transfer-internals.js +228 -0
- package/dist/files.d.ts +115 -0
- package/dist/files.js +563 -0
- package/dist/fresh-encrypted-request.d.ts +4 -0
- package/dist/fresh-encrypted-request.js +28 -0
- package/dist/grant-key-leases.d.ts +24 -0
- package/dist/grant-key-leases.js +144 -0
- package/dist/hosted-file-request.d.ts +6 -0
- package/dist/hosted-file-request.js +63 -0
- package/dist/hosted-operation-response.d.ts +5 -0
- package/dist/hosted-operation-response.js +26 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +10 -0
- package/dist/internal-types.d.ts +100 -0
- package/dist/internal-types.js +1 -0
- package/dist/local-file-transport.d.ts +37 -0
- package/dist/local-file-transport.js +457 -0
- package/dist/mdbase-connect.d.ts +28 -0
- package/dist/mdbase-connect.js +53 -0
- package/dist/notifications.d.ts +48 -0
- package/dist/notifications.js +52 -0
- package/dist/operation-helpers.d.ts +27 -0
- package/dist/operation-helpers.js +144 -0
- package/dist/operation-types.d.ts +707 -0
- package/dist/operation-types.js +1 -0
- package/dist/outcomes.d.ts +51 -0
- package/dist/outcomes.js +194 -0
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/pending-recovery-transports.d.ts +5 -0
- package/dist/pending-recovery-transports.js +52 -0
- package/dist/popup-authorization.d.ts +31 -0
- package/dist/popup-authorization.js +100 -0
- package/dist/query-pagination.d.ts +6 -0
- package/dist/query-pagination.js +119 -0
- package/dist/request-budget.d.ts +34 -0
- package/dist/request-budget.js +104 -0
- package/dist/request-coordinator.d.ts +29 -0
- package/dist/request-coordinator.js +213 -0
- package/dist/retired-grant-keys.d.ts +9 -0
- package/dist/retired-grant-keys.js +56 -0
- package/dist/runtime-utils.d.ts +52 -0
- package/dist/runtime-utils.js +368 -0
- package/dist/saved-view-pagination.d.ts +6 -0
- package/dist/saved-view-pagination.js +76 -0
- package/dist/selection.d.ts +48 -0
- package/dist/selection.js +112 -0
- package/dist/session.d.ts +114 -0
- package/dist/session.js +569 -0
- package/dist/shared-operation.d.ts +7 -0
- package/dist/shared-operation.js +41 -0
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +64 -0
- package/dist/structured-readiness.d.ts +77 -0
- package/dist/structured-readiness.js +126 -0
- package/dist/token-response.d.ts +12 -0
- package/dist/token-response.js +81 -0
- package/dist/transient-retry.d.ts +22 -0
- package/dist/transient-retry.js +59 -0
- package/dist/watch-policy.d.ts +8 -0
- package/dist/watch-policy.js +12 -0
- 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
|
+
}
|