@mdbase-dev/connect 0.1.0-beta.30 → 0.1.0-beta.32
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/README.md +75 -19
- package/dist/advanced.d.ts +7 -0
- package/dist/advanced.js +4 -0
- package/dist/application-session.d.ts +10 -5
- package/dist/application-session.js +32 -16
- package/dist/async.js +11 -5
- package/dist/authority-proof.d.ts +3 -0
- package/dist/authority-proof.js +24 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +3 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +5 -0
- package/dist/capabilities.js +2 -1
- package/dist/collection-client.d.ts +28 -28
- package/dist/collection-client.js +42 -42
- package/dist/connect-options.d.ts +12 -4
- package/dist/connect.d.ts +12 -4
- package/dist/connect.js +105 -104
- package/dist/connection-file-transport.d.ts +2 -2
- package/dist/connection-file-transport.js +4 -4
- package/dist/connection-notifications.d.ts +10 -4
- package/dist/connection-notifications.js +41 -23
- package/dist/connection-transport.d.ts +19 -15
- package/dist/connection-transport.js +202 -202
- package/dist/connection-types.d.ts +5 -4
- package/dist/connection.d.ts +43 -43
- package/dist/connection.js +221 -77
- package/dist/crypto-entry.d.ts +4 -0
- package/dist/crypto-entry.js +4 -0
- package/dist/crypto.js +4 -4
- package/dist/external-store.d.ts +10 -0
- package/dist/external-store.js +10 -0
- package/dist/file-transfer-internals.d.ts +22 -0
- package/dist/file-transfer-internals.js +186 -0
- package/dist/files.d.ts +16 -12
- package/dist/files.js +52 -206
- package/dist/hosted-file-request.js +3 -3
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -5
- package/dist/internal-types.d.ts +8 -1
- package/dist/local-file-transport.d.ts +2 -2
- package/dist/local-file-transport.js +13 -13
- package/dist/mdbase-connect.d.ts +6 -4
- package/dist/mdbase-connect.js +9 -8
- package/dist/notifications.d.ts +3 -2
- package/dist/operation-helpers.d.ts +3 -3
- package/dist/operation-helpers.js +12 -29
- package/dist/operation-types.d.ts +33 -8
- package/dist/outcomes.d.ts +15 -15
- package/dist/outcomes.js +13 -1
- package/dist/pending-mutation-store.d.ts +13 -0
- package/dist/pending-mutation-store.js +64 -0
- package/dist/request-budget.d.ts +27 -0
- package/dist/request-budget.js +95 -0
- package/dist/runtime-utils.d.ts +2 -0
- package/dist/runtime-utils.js +19 -0
- package/dist/session.d.ts +4 -3
- package/dist/session.js +4 -4
- package/dist/stored-token.d.ts +10 -0
- package/dist/stored-token.js +60 -0
- package/dist/token-response.d.ts +11 -0
- package/dist/token-response.js +77 -0
- package/package.json +12 -2
package/dist/outcomes.js
CHANGED
|
@@ -3,12 +3,15 @@ import { MdbaseConnectError, unknownConnectProblem } from "./errors.js";
|
|
|
3
3
|
export const COMMON_OPERATION_PROBLEM_CODES = [
|
|
4
4
|
"access_denied",
|
|
5
5
|
"access_paused",
|
|
6
|
+
"authorization_binding_incompatible",
|
|
6
7
|
"authorization_expired",
|
|
8
|
+
"capability_contract_incompatible",
|
|
7
9
|
"collection_access_denied",
|
|
8
10
|
"connector_identity_changed",
|
|
9
11
|
"connector_offline",
|
|
10
12
|
"connector_upgrade_required",
|
|
11
13
|
"direct_operation_rejected",
|
|
14
|
+
"durable_mutation_unsupported",
|
|
12
15
|
"operation_outcome_unknown",
|
|
13
16
|
"encryption_required",
|
|
14
17
|
"encrypted_relay_rejected",
|
|
@@ -18,6 +21,8 @@ export const COMMON_OPERATION_PROBLEM_CODES = [
|
|
|
18
21
|
"invalid_operation_response",
|
|
19
22
|
"invalid_request",
|
|
20
23
|
"missing_grant_key",
|
|
24
|
+
"mutation_recovery_expired",
|
|
25
|
+
"mutation_request_conflict",
|
|
21
26
|
"not_authorized",
|
|
22
27
|
"operation_cancelled",
|
|
23
28
|
"operation_failed",
|
|
@@ -26,11 +31,13 @@ export const COMMON_OPERATION_PROBLEM_CODES = [
|
|
|
26
31
|
"relay_unavailable",
|
|
27
32
|
"temporarily_unavailable",
|
|
28
33
|
"timeout",
|
|
34
|
+
"transport_protocol_incompatible",
|
|
29
35
|
"unsupported_operation"
|
|
30
36
|
];
|
|
31
37
|
export const ALL_CONNECT_PROBLEM_CODES = Object.keys(CONNECT_PROBLEM_CATALOG);
|
|
32
38
|
export const REGISTRATION_PROBLEM_CODES = [
|
|
33
39
|
"discovery_failed",
|
|
40
|
+
"invalid_operation_response",
|
|
34
41
|
"invalid_application_manifest",
|
|
35
42
|
"invalid_request",
|
|
36
43
|
"manifest_load_failed",
|
|
@@ -43,9 +50,11 @@ export const AUTHORIZATION_PROBLEM_CODES = [
|
|
|
43
50
|
"access_denied",
|
|
44
51
|
"application_identity_unavailable",
|
|
45
52
|
"approval_window_blocked",
|
|
53
|
+
"authorization_binding_incompatible",
|
|
46
54
|
"authorization_cancelled",
|
|
47
55
|
"authorization_replayed",
|
|
48
56
|
"browser_required",
|
|
57
|
+
"capability_contract_incompatible",
|
|
49
58
|
"collection_access_denied",
|
|
50
59
|
"collection_configuration_invalid",
|
|
51
60
|
"collection_contracts_missing",
|
|
@@ -60,6 +69,7 @@ export const AUTHORIZATION_PROBLEM_CODES = [
|
|
|
60
69
|
"connector_offline",
|
|
61
70
|
"connector_upgrade_required",
|
|
62
71
|
"device_authorization_failed",
|
|
72
|
+
"durable_mutation_unsupported",
|
|
63
73
|
"encryption_required",
|
|
64
74
|
"expired_token",
|
|
65
75
|
"invalid_application_authorization",
|
|
@@ -68,11 +78,13 @@ export const AUTHORIZATION_PROBLEM_CODES = [
|
|
|
68
78
|
"invalid_token_response",
|
|
69
79
|
"scope_denied",
|
|
70
80
|
"reconnect_required",
|
|
71
|
-
"token_exchange_failed"
|
|
81
|
+
"token_exchange_failed",
|
|
82
|
+
"transport_protocol_incompatible"
|
|
72
83
|
];
|
|
73
84
|
export const NOTIFICATION_PROBLEM_CODES = [
|
|
74
85
|
...REGISTRATION_PROBLEM_CODES,
|
|
75
86
|
"authorization_expired",
|
|
87
|
+
"invalid_operation_response",
|
|
76
88
|
"invalid_push_subscription",
|
|
77
89
|
"managed_fcm_not_declared",
|
|
78
90
|
"not_authorized",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type MutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { PendingMutation } from "./internal-types.js";
|
|
3
|
+
export declare class PendingMutationStore {
|
|
4
|
+
private readonly storage;
|
|
5
|
+
private readonly baseKey;
|
|
6
|
+
constructor(storage: Storage, baseKey: string);
|
|
7
|
+
list(collectionId: string | null): PendingMutation[];
|
|
8
|
+
find(collectionId: string | null, requestId: string): PendingMutation | null;
|
|
9
|
+
identifier(pending: PendingMutation): MutationOperationIdentifier;
|
|
10
|
+
store(pending: PendingMutation): void;
|
|
11
|
+
take(requestId: string): PendingMutation | null;
|
|
12
|
+
private key;
|
|
13
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isCollectionOperation, mutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { parseStored } from "./runtime-utils.js";
|
|
3
|
+
export class PendingMutationStore {
|
|
4
|
+
storage;
|
|
5
|
+
baseKey;
|
|
6
|
+
constructor(storage, baseKey) {
|
|
7
|
+
this.storage = storage;
|
|
8
|
+
this.baseKey = baseKey;
|
|
9
|
+
}
|
|
10
|
+
list(collectionId) {
|
|
11
|
+
if (!collectionId)
|
|
12
|
+
return [];
|
|
13
|
+
const legacy = parseStored(this.storage.getItem(this.baseKey));
|
|
14
|
+
if (validPendingMutation(legacy) && legacy.collectionId === collectionId) {
|
|
15
|
+
const migratedKey = this.key(legacy.requestId);
|
|
16
|
+
if (!this.storage.getItem(migratedKey)) {
|
|
17
|
+
this.storage.setItem(migratedKey, JSON.stringify(legacy));
|
|
18
|
+
}
|
|
19
|
+
this.storage.removeItem(this.baseKey);
|
|
20
|
+
}
|
|
21
|
+
const keys = [];
|
|
22
|
+
for (let index = 0; index < this.storage.length; index += 1) {
|
|
23
|
+
const key = this.storage.key(index);
|
|
24
|
+
if (key?.startsWith(`${this.baseKey}:`))
|
|
25
|
+
keys.push(key);
|
|
26
|
+
}
|
|
27
|
+
return keys
|
|
28
|
+
.map((key) => parseStored(this.storage.getItem(key)))
|
|
29
|
+
.filter((pending) => validPendingMutation(pending) && pending.collectionId === collectionId)
|
|
30
|
+
.sort((left, right) => left.createdAt - right.createdAt
|
|
31
|
+
|| left.requestId.localeCompare(right.requestId));
|
|
32
|
+
}
|
|
33
|
+
find(collectionId, requestId) {
|
|
34
|
+
return this.list(collectionId)
|
|
35
|
+
.find((pending) => pending.requestId === requestId) ?? null;
|
|
36
|
+
}
|
|
37
|
+
identifier(pending) {
|
|
38
|
+
return pending.mutation ?? mutationOperationIdentifier(pending.operation, pending.request?.input ?? (pending.operation === "sync" ? { action: "mutate" } : {})) ?? (pending.operation === "sync" ? "sync:mutate" : pending.operation);
|
|
39
|
+
}
|
|
40
|
+
store(pending) {
|
|
41
|
+
this.storage.setItem(this.key(pending.requestId), JSON.stringify(pending));
|
|
42
|
+
}
|
|
43
|
+
take(requestId) {
|
|
44
|
+
const key = this.key(requestId);
|
|
45
|
+
const pending = parseStored(this.storage.getItem(key));
|
|
46
|
+
this.storage.removeItem(key);
|
|
47
|
+
return pending;
|
|
48
|
+
}
|
|
49
|
+
key(requestId) {
|
|
50
|
+
return `${this.baseKey}:${encodeURIComponent(requestId)}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function validPendingMutation(pending) {
|
|
54
|
+
return Boolean(pending
|
|
55
|
+
&& typeof pending.collectionId === "string"
|
|
56
|
+
&& typeof pending.requestId === "string"
|
|
57
|
+
&& pending.requestId.length > 0
|
|
58
|
+
&& isCollectionOperation(pending.operation)
|
|
59
|
+
&& typeof pending.inputFingerprint === "string"
|
|
60
|
+
&& Number.isFinite(pending.createdAt)
|
|
61
|
+
&& pending.createdAt > 0
|
|
62
|
+
&& (pending.envelope || pending.request)
|
|
63
|
+
&& (pending.operation === "sync" || mutationOperationIdentifier(pending.operation, pending.request?.input ?? {}) !== null));
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
2
|
+
import type { MdbaseConnectTimeouts } from "./connect-options.js";
|
|
3
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
4
|
+
export declare const DEFAULT_STARTUP_TIMEOUT_MS = 10000;
|
|
5
|
+
export declare const DEFAULT_UPLOAD_TIMEOUT_MS = 120000;
|
|
6
|
+
export declare const DEFAULT_SYNC_TIMEOUT_MS = 60000;
|
|
7
|
+
export interface ResolvedConnectTimeouts {
|
|
8
|
+
requestMs: number | null;
|
|
9
|
+
watchStartMs: number | null;
|
|
10
|
+
uploadMs: number | null;
|
|
11
|
+
syncMs: number | null;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveConnectTimeouts(timeouts?: MdbaseConnectTimeouts): ResolvedConnectTimeouts;
|
|
14
|
+
export interface RequestBudget {
|
|
15
|
+
readonly signal: AbortSignal;
|
|
16
|
+
readonly deadline: number | null;
|
|
17
|
+
remainingMs(): number | null;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function createRequestBudget(options?: ConnectRequestOptions, defaultTimeoutMs?: number | null, now?: () => number): RequestBudget;
|
|
21
|
+
export declare function withRequestBudget<Result>(options: ConnectRequestOptions | undefined, defaultTimeoutMs: number | null, operation: (budget: RequestBudget) => Promise<Result>): Promise<Result>;
|
|
22
|
+
/**
|
|
23
|
+
* Budget an operation whose transport cooperates with AbortSignal and must run
|
|
24
|
+
* its own bounded cleanup/recovery before the public promise settles.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withCooperativeRequestBudget<Result>(options: ConnectRequestOptions | undefined, defaultTimeoutMs: number | null, operation: (budget: RequestBudget) => Promise<Result>): Promise<Result>;
|
|
27
|
+
export declare function requestAbortReason(signal: AbortSignal): unknown;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { MdbaseConnectError, connectError } from "./errors.js";
|
|
2
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
3
|
+
export const DEFAULT_STARTUP_TIMEOUT_MS = 10_000;
|
|
4
|
+
export const DEFAULT_UPLOAD_TIMEOUT_MS = 120_000;
|
|
5
|
+
export const DEFAULT_SYNC_TIMEOUT_MS = 60_000;
|
|
6
|
+
export function resolveConnectTimeouts(timeouts = {}) {
|
|
7
|
+
return {
|
|
8
|
+
requestMs: configuredTimeout(timeouts.requestMs, DEFAULT_REQUEST_TIMEOUT_MS, "requestMs"),
|
|
9
|
+
watchStartMs: configuredTimeout(timeouts.watchStartMs, DEFAULT_STARTUP_TIMEOUT_MS, "watchStartMs"),
|
|
10
|
+
uploadMs: configuredTimeout(timeouts.uploadMs, DEFAULT_UPLOAD_TIMEOUT_MS, "uploadMs"),
|
|
11
|
+
syncMs: configuredTimeout(timeouts.syncMs, DEFAULT_SYNC_TIMEOUT_MS, "syncMs")
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createRequestBudget(options = {}, defaultTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, now = Date.now) {
|
|
15
|
+
const configured = options.timeoutMs === undefined ? defaultTimeoutMs : options.timeoutMs;
|
|
16
|
+
const timeoutMs = configured === null ? null : validTimeout(configured);
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const deadline = timeoutMs === null ? null : now() + timeoutMs;
|
|
19
|
+
const abortFromCaller = () => controller.abort(options.signal?.reason);
|
|
20
|
+
if (options.signal?.aborted)
|
|
21
|
+
abortFromCaller();
|
|
22
|
+
else
|
|
23
|
+
options.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
24
|
+
const timer = timeoutMs === null
|
|
25
|
+
? undefined
|
|
26
|
+
: setTimeout(() => controller.abort(connectError("timeout", "The operation exceeded its request deadline.", { operationOutcome: "not_sent" })), timeoutMs);
|
|
27
|
+
let disposed = false;
|
|
28
|
+
return {
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
deadline,
|
|
31
|
+
remainingMs: () => deadline === null ? null : Math.max(0, deadline - now()),
|
|
32
|
+
dispose: () => {
|
|
33
|
+
if (disposed)
|
|
34
|
+
return;
|
|
35
|
+
disposed = true;
|
|
36
|
+
if (timer !== undefined)
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function withRequestBudget(options, defaultTimeoutMs, operation) {
|
|
43
|
+
const budget = createRequestBudget(options, defaultTimeoutMs);
|
|
44
|
+
let rejectOnAbort;
|
|
45
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
46
|
+
rejectOnAbort = reject;
|
|
47
|
+
});
|
|
48
|
+
const abort = () => rejectOnAbort?.(requestAbortReason(budget.signal));
|
|
49
|
+
budget.signal.addEventListener("abort", abort, { once: true });
|
|
50
|
+
try {
|
|
51
|
+
if (budget.signal.aborted)
|
|
52
|
+
throw requestAbortReason(budget.signal);
|
|
53
|
+
return await Promise.race([operation(budget), aborted]);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
budget.signal.removeEventListener("abort", abort);
|
|
57
|
+
budget.dispose();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Budget an operation whose transport cooperates with AbortSignal and must run
|
|
62
|
+
* its own bounded cleanup/recovery before the public promise settles.
|
|
63
|
+
*/
|
|
64
|
+
export async function withCooperativeRequestBudget(options, defaultTimeoutMs, operation) {
|
|
65
|
+
const budget = createRequestBudget(options, defaultTimeoutMs);
|
|
66
|
+
try {
|
|
67
|
+
if (budget.signal.aborted)
|
|
68
|
+
throw requestAbortReason(budget.signal);
|
|
69
|
+
return await operation(budget);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
budget.dispose();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function requestAbortReason(signal) {
|
|
76
|
+
if (signal.reason instanceof MdbaseConnectError)
|
|
77
|
+
return signal.reason;
|
|
78
|
+
return connectError("operation_cancelled", "The operation was cancelled before it changed the collection.", { operationOutcome: "not_sent", cause: signal.reason });
|
|
79
|
+
}
|
|
80
|
+
function validTimeout(value) {
|
|
81
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
82
|
+
throw new TypeError("timeoutMs must be a positive safe integer or null.");
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function configuredTimeout(value, fallback, name) {
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
return fallback;
|
|
89
|
+
if (value === null)
|
|
90
|
+
return null;
|
|
91
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
92
|
+
throw new TypeError(`${name} must be a positive safe integer or null.`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
package/dist/runtime-utils.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { MdbaseDeviceAuthorization } from "./authorization-types.js";
|
|
|
5
5
|
type NetworkProblemCode = "notification_registration_failed" | "notification_unregistration_failed" | "notifications_unavailable" | "temporarily_unavailable";
|
|
6
6
|
/** Normalize fetch rejection at the exact I/O boundary. */
|
|
7
7
|
export declare function connectFetch(input: RequestInfo | URL, init: RequestInit | undefined, code: NetworkProblemCode, message: string): Promise<Response>;
|
|
8
|
+
/** Decode JSON without allowing native syntax/body errors across the SDK boundary. */
|
|
9
|
+
export declare function decodeJsonResponse(response: Response, code: string, message: string): Promise<any>;
|
|
8
10
|
export declare function canonicalLoopbackUrl(value: string): string;
|
|
9
11
|
export declare function createPkce(): Promise<{
|
|
10
12
|
verifier: string;
|
package/dist/runtime-utils.js
CHANGED
|
@@ -11,6 +11,25 @@ export async function connectFetch(input, init, code, message) {
|
|
|
11
11
|
throw connectError(code, message, { cause });
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/** Decode JSON without allowing native syntax/body errors across the SDK boundary. */
|
|
15
|
+
export async function decodeJsonResponse(response, code, message) {
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
text = await response.text();
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
throw serverConnectError(code, message, { status: response.status, cause });
|
|
22
|
+
}
|
|
23
|
+
if (text.trim() === "") {
|
|
24
|
+
throw serverConnectError(code, message, { status: response.status });
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
throw serverConnectError(code, message, { status: response.status, cause });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
export function canonicalLoopbackUrl(value) {
|
|
15
34
|
const url = new URL(value);
|
|
16
35
|
if (url.protocol !== "http:"
|
package/dist/session.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { MdbaseConnection, type MdbaseAuthorizationOutcome, type MdbaseAuthoriza
|
|
|
3
3
|
import type { MdbaseAuthorizationCapabilities, MdbaseConnectionInfo } from "./connection-types.js";
|
|
4
4
|
import { type AuthorizationProblemCode, type ConnectOutcome, type SessionProblemCode } from "./outcomes.js";
|
|
5
5
|
import type { MdbaseSelectionHistory, MdbaseApplicationSelection } from "./selection.js";
|
|
6
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
6
7
|
export interface MdbaseSessionConnect<Frontmatter extends JsonObject> {
|
|
7
8
|
authorize(options?: MdbaseAuthorizeOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, AuthorizationProblemCode>>;
|
|
8
|
-
completeAuthorization(callbackUrl: string): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
|
|
9
|
+
completeAuthorization(callbackUrl: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
|
|
9
10
|
connections(): MdbaseConnectionInfo[];
|
|
10
11
|
connection(collectionId: string): MdbaseConnection<Frontmatter> | null;
|
|
11
12
|
unavailableReason(collectionId: string): MdbaseUnavailableReason | null;
|
|
@@ -52,7 +53,7 @@ export declare class MdbaseSession<Frontmatter extends JsonObject = JsonObject>
|
|
|
52
53
|
private transactionDepth;
|
|
53
54
|
private callbackPromise;
|
|
54
55
|
constructor(connect: MdbaseSessionConnect<Frontmatter>, options: MdbaseSessionOptions);
|
|
55
|
-
start(): Promise<ConnectOutcome<MdbaseSessionSnapshot<Frontmatter>, SessionProblemCode>>;
|
|
56
|
+
start(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseSessionSnapshot<Frontmatter>, SessionProblemCode>>;
|
|
56
57
|
destroy(): void;
|
|
57
58
|
getSnapshot(): MdbaseSessionSnapshot<Frontmatter>;
|
|
58
59
|
subscribe(listener: () => void): () => void;
|
|
@@ -70,7 +71,7 @@ export declare class MdbaseSession<Frontmatter extends JsonObject = JsonObject>
|
|
|
70
71
|
kind: "unchanged";
|
|
71
72
|
connection: MdbaseConnection<Frontmatter>;
|
|
72
73
|
}, SessionProblemCode>>;
|
|
73
|
-
handleAuthorizationCallback(callbackUrl: string): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
|
|
74
|
+
handleAuthorizationCallback(callbackUrl: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, AuthorizationProblemCode>>;
|
|
74
75
|
private autoSelectOnlyConnection;
|
|
75
76
|
private refresh;
|
|
76
77
|
private refreshOnce;
|
package/dist/session.js
CHANGED
|
@@ -29,7 +29,7 @@ export class MdbaseSession {
|
|
|
29
29
|
this.snapshot = { status: "unselected", connections: [] };
|
|
30
30
|
this.refresh();
|
|
31
31
|
}
|
|
32
|
-
async start() {
|
|
32
|
+
async start(options) {
|
|
33
33
|
if (!this.started) {
|
|
34
34
|
this.started = true;
|
|
35
35
|
this.stopConnections = this.connect.onConnectionsChange(() => this.refresh());
|
|
@@ -40,7 +40,7 @@ export class MdbaseSession {
|
|
|
40
40
|
}
|
|
41
41
|
const callback = this.selection.authorizationCallback();
|
|
42
42
|
if (callback) {
|
|
43
|
-
const completed = await this.handleAuthorizationCallback(callback);
|
|
43
|
+
const completed = await this.handleAuthorizationCallback(callback, options);
|
|
44
44
|
if (!completed.ok)
|
|
45
45
|
return completed;
|
|
46
46
|
}
|
|
@@ -151,14 +151,14 @@ export class MdbaseSession {
|
|
|
151
151
|
this.refresh();
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
handleAuthorizationCallback(callbackUrl) {
|
|
154
|
+
handleAuthorizationCallback(callbackUrl, options) {
|
|
155
155
|
if (!isAuthorizationCallbackUrl(callbackUrl)) {
|
|
156
156
|
return Promise.resolve(connectFailure(connectProblem("invalid_callback", "This URL is not an authorization callback.")));
|
|
157
157
|
}
|
|
158
158
|
if (this.callbackPromise)
|
|
159
159
|
return this.callbackPromise;
|
|
160
160
|
this.transactionDepth += 1;
|
|
161
|
-
const completion = this.connect.completeAuthorization(callbackUrl)
|
|
161
|
+
const completion = this.connect.completeAuthorization(callbackUrl, options)
|
|
162
162
|
.then((outcome) => {
|
|
163
163
|
if (!outcome.ok) {
|
|
164
164
|
const returnTo = authorizationReturnToFromProblem(outcome.problem);
|
|
@@ -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): 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,60 @@
|
|
|
1
|
+
import { 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) => {
|
|
5
|
+
invalidate(typeof keyHandle === "string" ? keyHandle : undefined);
|
|
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
|
+
if (!parseGrantScope(token.scope))
|
|
31
|
+
return reject(token.keyHandle);
|
|
32
|
+
if (token.fileCapability && !validFileCapability(token.fileCapability)) {
|
|
33
|
+
return reject(token.keyHandle);
|
|
34
|
+
}
|
|
35
|
+
if (token.authority && !validStoredAuthority(token.authority, token.collectionId)) {
|
|
36
|
+
return reject(token.keyHandle);
|
|
37
|
+
}
|
|
38
|
+
if (relayEncryption === "required") {
|
|
39
|
+
if (token.authority) {
|
|
40
|
+
if (!token.keyHandle || !token.authority.proofPublicKey) {
|
|
41
|
+
return reject(token.keyHandle);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (!token.grantId
|
|
45
|
+
|| !token.keyHandle
|
|
46
|
+
|| !validStoredEncryption(token.encryption, token.collectionId)) {
|
|
47
|
+
return reject(token.keyHandle);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (token.expiresAt <= Date.now()
|
|
51
|
+
&& (!token.refreshToken || (token.refreshExpiresAt ?? 0) <= Date.now())) {
|
|
52
|
+
// The cloud bearer and local grant proof have separate lifetimes. Keep an
|
|
53
|
+
// encrypted local grant usable while the connector still recognizes it.
|
|
54
|
+
if (directCapable(token))
|
|
55
|
+
return token;
|
|
56
|
+
invalidate(token.keyHandle);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return token;
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
defaultApplicationOrigin: string;
|
|
8
|
+
pinConnectorIdentity(connectorId: string, publicKey: string): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function storedTokenFromResponse({ body, clientId, keyHandle, previous, defaultApplicationOrigin, pinConnectorIdentity }: StoredTokenResponseOptions): StoredToken;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { validateGrantEncryption } from "./crypto.js";
|
|
2
|
+
import { connectError } from "./errors.js";
|
|
3
|
+
import { parseGrantScope, validAuthorityTokenResponse, validFileCapability } from "./runtime-utils.js";
|
|
4
|
+
export function storedTokenFromResponse({ body, clientId, keyHandle, previous, 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 (body.authority && !validAuthorityTokenResponse(body.authority, collectionId)) {
|
|
14
|
+
throw connectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
|
|
15
|
+
}
|
|
16
|
+
if (body.authority && body.encryption) {
|
|
17
|
+
throw connectError("invalid_token_response", "Authorization returned conflicting collection transports.");
|
|
18
|
+
}
|
|
19
|
+
if (body.file_capability !== null && body.file_capability !== undefined
|
|
20
|
+
&& !validFileCapability(body.file_capability)) {
|
|
21
|
+
throw connectError("invalid_token_response", "Authorization returned an invalid file capability.");
|
|
22
|
+
}
|
|
23
|
+
if (body.encryption) {
|
|
24
|
+
try {
|
|
25
|
+
validateGrantEncryption(body.encryption);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw connectError("invalid_token_response", "Authorization returned an invalid encrypted relay binding.");
|
|
29
|
+
}
|
|
30
|
+
pinConnectorIdentity(body.encryption.connector_id, body.encryption.connector_agreement_public_key);
|
|
31
|
+
if (body.encryption.collection_id !== collectionId
|
|
32
|
+
|| typeof body.grant_id !== "string"
|
|
33
|
+
|| body.grant_id.length === 0) {
|
|
34
|
+
throw connectError("invalid_token_response", "Authorization returned an encrypted relay binding for another grant.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (previous?.encryption
|
|
38
|
+
&& body.encryption
|
|
39
|
+
&& previous.keyHandle
|
|
40
|
+
&& previous.keyHandle === keyHandle
|
|
41
|
+
&& (previous.grantId !== body.grant_id
|
|
42
|
+
|| previous.encryption.connector_id !== body.encryption.connector_id
|
|
43
|
+
|| previous.encryption.connector_agreement_public_key
|
|
44
|
+
!== body.encryption.connector_agreement_public_key
|
|
45
|
+
|| previous.encryption.application_agreement_public_key
|
|
46
|
+
!== body.encryption.application_agreement_public_key)) {
|
|
47
|
+
throw connectError("connector_identity_changed", "The connector identity changed during authorization renewal. Reauthorize before sending collection data.");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
version: 1,
|
|
51
|
+
accessToken: body.access_token,
|
|
52
|
+
refreshToken: body.refresh_token,
|
|
53
|
+
clientId,
|
|
54
|
+
collectionId,
|
|
55
|
+
collectionName: body.collection_name ?? `Collection ${collectionId.slice(0, 8)}`,
|
|
56
|
+
operations: body.operations,
|
|
57
|
+
scope,
|
|
58
|
+
expiresAt: Date.now() + body.expires_in * 1_000,
|
|
59
|
+
refreshExpiresAt: body.refresh_expires_in
|
|
60
|
+
? Date.now() + body.refresh_expires_in * 1_000
|
|
61
|
+
: undefined,
|
|
62
|
+
grantId: body.grant_id,
|
|
63
|
+
encryption: body.encryption ?? undefined,
|
|
64
|
+
fileCapability: body.file_capability ?? undefined,
|
|
65
|
+
applicationOrigin: body.application_origin ?? defaultApplicationOrigin,
|
|
66
|
+
keyHandle,
|
|
67
|
+
savedAt: Date.now(),
|
|
68
|
+
authority: body.authority ? {
|
|
69
|
+
operationsUrl: body.authority.operations_url,
|
|
70
|
+
syncUrl: body.authority.sync_url,
|
|
71
|
+
filesUrl: body.authority.files_url,
|
|
72
|
+
replicaId: body.authority.replica_id,
|
|
73
|
+
accessToken: body.authority.access_token,
|
|
74
|
+
proofPublicKey: body.authority.proof_public_key
|
|
75
|
+
} : undefined
|
|
76
|
+
};
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mdbase-dev/connect",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.32",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
".": {
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
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"
|
|
21
29
|
}
|
|
22
30
|
},
|
|
23
31
|
"files": [
|
|
@@ -25,7 +33,7 @@
|
|
|
25
33
|
"README.md"
|
|
26
34
|
],
|
|
27
35
|
"dependencies": {
|
|
28
|
-
"@mdbase-dev/connect-protocol": "0.1.0-beta.
|
|
36
|
+
"@mdbase-dev/connect-protocol": "0.1.0-beta.32"
|
|
29
37
|
},
|
|
30
38
|
"devDependencies": {
|
|
31
39
|
"esbuild": "^0.28.1",
|
|
@@ -36,6 +44,8 @@
|
|
|
36
44
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/build-browser.mjs",
|
|
37
45
|
"test": "vitest run",
|
|
38
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 && tsc -p tsconfig.public-api.json",
|
|
39
49
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
40
50
|
}
|
|
41
51
|
}
|