@mdbase-dev/connect 0.1.0-beta.31 → 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.
Files changed (64) hide show
  1. package/README.md +75 -19
  2. package/dist/advanced.d.ts +7 -0
  3. package/dist/advanced.js +4 -0
  4. package/dist/application-session.d.ts +10 -5
  5. package/dist/application-session.js +32 -16
  6. package/dist/async.js +11 -5
  7. package/dist/authority-proof.d.ts +3 -0
  8. package/dist/authority-proof.js +24 -0
  9. package/dist/browser/integrity.json +1 -1
  10. package/dist/browser/mdbase-connect.min.js +3 -3
  11. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  12. package/dist/browser.d.ts +3 -0
  13. package/dist/browser.js +5 -0
  14. package/dist/capabilities.js +2 -1
  15. package/dist/collection-client.d.ts +28 -28
  16. package/dist/collection-client.js +42 -42
  17. package/dist/connect-options.d.ts +12 -4
  18. package/dist/connect.d.ts +12 -4
  19. package/dist/connect.js +105 -104
  20. package/dist/connection-file-transport.d.ts +2 -2
  21. package/dist/connection-file-transport.js +4 -4
  22. package/dist/connection-notifications.d.ts +10 -4
  23. package/dist/connection-notifications.js +41 -23
  24. package/dist/connection-transport.d.ts +19 -15
  25. package/dist/connection-transport.js +202 -202
  26. package/dist/connection-types.d.ts +5 -4
  27. package/dist/connection.d.ts +43 -43
  28. package/dist/connection.js +221 -77
  29. package/dist/crypto-entry.d.ts +4 -0
  30. package/dist/crypto-entry.js +4 -0
  31. package/dist/crypto.js +4 -4
  32. package/dist/external-store.d.ts +10 -0
  33. package/dist/external-store.js +10 -0
  34. package/dist/file-transfer-internals.d.ts +22 -0
  35. package/dist/file-transfer-internals.js +186 -0
  36. package/dist/files.d.ts +16 -12
  37. package/dist/files.js +52 -206
  38. package/dist/hosted-file-request.js +3 -3
  39. package/dist/index.d.ts +2 -5
  40. package/dist/index.js +1 -5
  41. package/dist/internal-types.d.ts +8 -1
  42. package/dist/local-file-transport.d.ts +2 -2
  43. package/dist/local-file-transport.js +13 -13
  44. package/dist/mdbase-connect.d.ts +6 -4
  45. package/dist/mdbase-connect.js +9 -8
  46. package/dist/notifications.d.ts +3 -2
  47. package/dist/operation-helpers.d.ts +3 -3
  48. package/dist/operation-helpers.js +12 -29
  49. package/dist/operation-types.d.ts +33 -8
  50. package/dist/outcomes.d.ts +15 -15
  51. package/dist/outcomes.js +13 -1
  52. package/dist/pending-mutation-store.d.ts +13 -0
  53. package/dist/pending-mutation-store.js +64 -0
  54. package/dist/request-budget.d.ts +27 -0
  55. package/dist/request-budget.js +95 -0
  56. package/dist/runtime-utils.d.ts +2 -0
  57. package/dist/runtime-utils.js +19 -0
  58. package/dist/session.d.ts +4 -3
  59. package/dist/session.js +4 -4
  60. package/dist/stored-token.d.ts +10 -0
  61. package/dist/stored-token.js +60 -0
  62. package/dist/token-response.d.ts +11 -0
  63. package/dist/token-response.js +77 -0
  64. 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
+ }
@@ -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;
@@ -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.31",
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.31"
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
  }