@telecrypt-io/storage 0.4.0 → 0.5.8

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.
@@ -0,0 +1,16 @@
1
+ /** Maximum plaintext media body accepted by the non-streaming SDK surface. */
2
+ export declare const MAX_MEDIA_FILE_BYTES: number;
3
+ /** Matrix's canonical identifier bounds used at authenticated-client boundaries. */
4
+ export declare const MAX_MATRIX_USER_ID_BYTES = 255;
5
+ export declare const MAX_MATRIX_DEVICE_ID_LENGTH = 128;
6
+ export declare const MAX_MATRIX_IDENTIFIER_LENGTH = 512;
7
+ /**
8
+ * Validates a new/current Matrix identity and binds its server name exactly to
9
+ * the caller's independently trusted Matrix topology. The Matrix server name
10
+ * is deliberately explicit: a deployment may expose its homeserver through a
11
+ * backend hostname that differs from the user-visible Matrix server name.
12
+ */
13
+ export declare function validateCanonicalMatrixUserId(value: unknown, serverName: unknown): string;
14
+ export declare function validateMatrixDeviceId(value: unknown): string;
15
+ export declare function validateMatrixRoomId(value: unknown, name?: string): string;
16
+ export declare function validateMatrixEventId(value: unknown, name?: string): string;
@@ -0,0 +1,54 @@
1
+ /** Maximum plaintext media body accepted by the non-streaming SDK surface. */
2
+ export const MAX_MEDIA_FILE_BYTES = 128 * 1024 * 1024;
3
+ /** Matrix's canonical identifier bounds used at authenticated-client boundaries. */
4
+ export const MAX_MATRIX_USER_ID_BYTES = 255;
5
+ export const MAX_MATRIX_DEVICE_ID_LENGTH = 128;
6
+ export const MAX_MATRIX_IDENTIFIER_LENGTH = 512;
7
+ const MATRIX_SERVER_NAME_PATTERN = "(?:\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9.-]+)(?::\\d{1,5})?";
8
+ const MATRIX_SERVER_NAME_REGEXP = new RegExp(`^${MATRIX_SERVER_NAME_PATTERN}$`);
9
+ const MATRIX_USER_ID_PATTERN = new RegExp(`^@([A-Za-z0-9._=+\\-/]+):(${MATRIX_SERVER_NAME_PATTERN})$`);
10
+ const MATRIX_DEVICE_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
11
+ const MATRIX_ROOM_ID_PATTERN = new RegExp(`^![A-Za-z0-9._~+\\/-]+:${MATRIX_SERVER_NAME_PATTERN}$`);
12
+ const MATRIX_EVENT_ID_PATTERN = /^\$[A-Za-z0-9._~:/+\-]{1,511}$/;
13
+ /**
14
+ * Validates a new/current Matrix identity and binds its server name exactly to
15
+ * the caller's independently trusted Matrix topology. The Matrix server name
16
+ * is deliberately explicit: a deployment may expose its homeserver through a
17
+ * backend hostname that differs from the user-visible Matrix server name.
18
+ */
19
+ export function validateCanonicalMatrixUserId(value, serverName) {
20
+ if (typeof value !== "string" ||
21
+ new TextEncoder().encode(value).byteLength > MAX_MATRIX_USER_ID_BYTES) {
22
+ throw new Error("invalid Matrix user ID");
23
+ }
24
+ if (typeof serverName !== "string" || !MATRIX_SERVER_NAME_REGEXP.test(serverName)) {
25
+ throw new Error("invalid Matrix server name");
26
+ }
27
+ const match = value.match(MATRIX_USER_ID_PATTERN);
28
+ if (!match || match[2].toLowerCase() !== serverName.toLowerCase()) {
29
+ throw new Error("invalid Matrix user ID for this homeserver");
30
+ }
31
+ return value;
32
+ }
33
+ export function validateMatrixDeviceId(value) {
34
+ if (typeof value !== "string" || value.length > MAX_MATRIX_DEVICE_ID_LENGTH || !MATRIX_DEVICE_ID_PATTERN.test(value)) {
35
+ throw new Error("invalid Matrix device ID");
36
+ }
37
+ return value;
38
+ }
39
+ export function validateMatrixRoomId(value, name = "room ID") {
40
+ if (typeof value !== "string" ||
41
+ value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
42
+ !MATRIX_ROOM_ID_PATTERN.test(value)) {
43
+ throw new Error(`invalid Matrix ${name}`);
44
+ }
45
+ return value;
46
+ }
47
+ export function validateMatrixEventId(value, name = "event ID") {
48
+ if (typeof value !== "string" ||
49
+ value.length > MAX_MATRIX_IDENTIFIER_LENGTH ||
50
+ !MATRIX_EVENT_ID_PATTERN.test(value)) {
51
+ throw new Error(`invalid Matrix ${name}`);
52
+ }
53
+ return value;
54
+ }
@@ -5,3 +5,69 @@
5
5
  export declare class StorageError extends Error {
6
6
  constructor(message: string);
7
7
  }
8
+ /** Setup would replace account recovery state, which this SDK never does. */
9
+ export declare class RecoveryAlreadyConfiguredError extends StorageError {
10
+ readonly code: "RECOVERY_ALREADY_CONFIGURED";
11
+ constructor();
12
+ }
13
+ /** Restore failed without exposing provider or crypto-library error details. */
14
+ export declare class RecoveryRestoreError extends StorageError {
15
+ readonly code: "RECOVERY_RESTORE_FAILED";
16
+ constructor();
17
+ }
18
+ /** Setup failed before the generated Recovery Key could be safely returned. */
19
+ export declare class RecoverySetupError extends StorageError {
20
+ readonly code: "RECOVERY_SETUP_FAILED";
21
+ constructor();
22
+ }
23
+ /** The recovery setup transport result is ambiguous; state must be checked before retrying. */
24
+ export declare class RecoverySetupAmbiguousError extends StorageError {
25
+ readonly code: "RECOVERY_SETUP_AMBIGUOUS";
26
+ constructor();
27
+ }
28
+ /** Recovery restore was cancelled after the provider may have changed state. */
29
+ export declare class RecoveryRestoreAmbiguousError extends StorageError {
30
+ readonly code: "RECOVERY_RESTORE_AMBIGUOUS";
31
+ constructor();
32
+ }
33
+ /** A Matrix mutation was cancelled after its request started. */
34
+ export declare class MutationOutcomeUnknownError extends StorageError {
35
+ readonly code: "MUTATION_OUTCOME_UNKNOWN";
36
+ readonly operation: string;
37
+ constructor(operation?: string);
38
+ }
39
+ /** A multi-room mutation committed some work before a later step failed. */
40
+ export declare class MutationPartialError extends StorageError {
41
+ readonly code: "MUTATION_PARTIAL";
42
+ readonly operation: string;
43
+ readonly completedIds: readonly string[];
44
+ constructor(operation: string, completedIds: readonly string[], detail?: string);
45
+ }
46
+ /** A vault or folder still contains files or child folders. */
47
+ export declare class NonEmptyTreeError extends StorageError {
48
+ readonly code: "NON_EMPTY_TREE";
49
+ readonly treeId: string;
50
+ constructor(treeId: string);
51
+ }
52
+ /** Room creation timed out or returned an unusable identity. */
53
+ export declare class RoomCreationAmbiguousError extends StorageError {
54
+ readonly code: "ROOM_CREATION_AMBIGUOUS";
55
+ readonly operation: string;
56
+ constructor(operation?: string);
57
+ }
58
+ /** A newly-created room could not be fully rolled back. */
59
+ export declare class RoomCleanupIncompleteError extends StorageError {
60
+ readonly code: "ROOM_CLEANUP_INCOMPLETE";
61
+ readonly roomId: string;
62
+ constructor(roomId: string);
63
+ }
64
+ /** The SDK's single product file-size limit was exceeded. */
65
+ export declare class FileTooLargeError extends StorageError {
66
+ readonly code: "FILE_TOO_LARGE";
67
+ constructor();
68
+ }
69
+ /** The file event is present, but this device has no key to decrypt it. */
70
+ export declare class UndecryptableFileError extends StorageError {
71
+ readonly code: "FILE_UNDECRYPTABLE";
72
+ constructor();
73
+ }
@@ -8,3 +8,111 @@ export class StorageError extends Error {
8
8
  this.name = "StorageError";
9
9
  }
10
10
  }
11
+ /** Setup would replace account recovery state, which this SDK never does. */
12
+ export class RecoveryAlreadyConfiguredError extends StorageError {
13
+ code = "RECOVERY_ALREADY_CONFIGURED";
14
+ constructor() {
15
+ super("recovery is already configured for this account; use the existing Recovery Key or restore it on this device");
16
+ this.name = "RecoveryAlreadyConfiguredError";
17
+ }
18
+ }
19
+ /** Restore failed without exposing provider or crypto-library error details. */
20
+ export class RecoveryRestoreError extends StorageError {
21
+ code = "RECOVERY_RESTORE_FAILED";
22
+ constructor() {
23
+ super("recovery restore failed; verify the Recovery Key and account");
24
+ this.name = "RecoveryRestoreError";
25
+ }
26
+ }
27
+ /** Setup failed before the generated Recovery Key could be safely returned. */
28
+ export class RecoverySetupError extends StorageError {
29
+ code = "RECOVERY_SETUP_FAILED";
30
+ constructor() {
31
+ super("recovery setup did not complete; check recovery status before retrying");
32
+ this.name = "RecoverySetupError";
33
+ }
34
+ }
35
+ /** The recovery setup transport result is ambiguous; state must be checked before retrying. */
36
+ export class RecoverySetupAmbiguousError extends StorageError {
37
+ code = "RECOVERY_SETUP_AMBIGUOUS";
38
+ constructor() {
39
+ super("recovery setup may have completed; check recovery status before retrying");
40
+ this.name = "RecoverySetupAmbiguousError";
41
+ }
42
+ }
43
+ /** Recovery restore was cancelled after the provider may have changed state. */
44
+ export class RecoveryRestoreAmbiguousError extends StorageError {
45
+ code = "RECOVERY_RESTORE_AMBIGUOUS";
46
+ constructor() {
47
+ super("recovery restore may have completed; check recovery status before retrying");
48
+ this.name = "RecoveryRestoreAmbiguousError";
49
+ }
50
+ }
51
+ /** A Matrix mutation was cancelled after its request started. */
52
+ export class MutationOutcomeUnknownError extends StorageError {
53
+ code = "MUTATION_OUTCOME_UNKNOWN";
54
+ operation;
55
+ constructor(operation = "operation") {
56
+ super(`${operation} outcome is unknown; reconcile current state before retrying`);
57
+ this.name = "MutationOutcomeUnknownError";
58
+ this.operation = operation;
59
+ }
60
+ }
61
+ /** A multi-room mutation committed some work before a later step failed. */
62
+ export class MutationPartialError extends StorageError {
63
+ code = "MUTATION_PARTIAL";
64
+ operation;
65
+ completedIds;
66
+ constructor(operation, completedIds, detail) {
67
+ super(`${operation} failed after partial completion${detail ? `: ${detail}` : ""}; retry to reconcile current state`);
68
+ this.name = "MutationPartialError";
69
+ this.operation = operation;
70
+ this.completedIds = [...completedIds];
71
+ }
72
+ }
73
+ /** A vault or folder still contains files or child folders. */
74
+ export class NonEmptyTreeError extends StorageError {
75
+ code = "NON_EMPTY_TREE";
76
+ treeId;
77
+ constructor(treeId) {
78
+ super("cannot delete a nonempty vault or folder; delete its files and empty child folders first");
79
+ this.name = "NonEmptyTreeError";
80
+ this.treeId = treeId;
81
+ }
82
+ }
83
+ /** Room creation timed out or returned an unusable identity. */
84
+ export class RoomCreationAmbiguousError extends StorageError {
85
+ code = "ROOM_CREATION_AMBIGUOUS";
86
+ operation;
87
+ constructor(operation = "room creation") {
88
+ super(`${operation} outcome is unknown; reconcile newly-created rooms before retrying`);
89
+ this.name = "RoomCreationAmbiguousError";
90
+ this.operation = operation;
91
+ }
92
+ }
93
+ /** A newly-created room could not be fully rolled back. */
94
+ export class RoomCleanupIncompleteError extends StorageError {
95
+ code = "ROOM_CLEANUP_INCOMPLETE";
96
+ roomId;
97
+ constructor(roomId) {
98
+ super("created room cleanup is incomplete; retry cleanup for the created room");
99
+ this.name = "RoomCleanupIncompleteError";
100
+ this.roomId = roomId;
101
+ }
102
+ }
103
+ /** The SDK's single product file-size limit was exceeded. */
104
+ export class FileTooLargeError extends StorageError {
105
+ code = "FILE_TOO_LARGE";
106
+ constructor() {
107
+ super("file exceeds the 128 MiB limit");
108
+ this.name = "FileTooLargeError";
109
+ }
110
+ }
111
+ /** The file event is present, but this device has no key to decrypt it. */
112
+ export class UndecryptableFileError extends StorageError {
113
+ code = "FILE_UNDECRYPTABLE";
114
+ constructor() {
115
+ super("downloadFile: could not read file info from the event — it is likely undecryptable on this device (missing megolm session; try restoring from a Recovery Key)");
116
+ this.name = "UndecryptableFileError";
117
+ }
118
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Read a response body without allowing an untrusted peer to control the
3
+ * amount of memory consumed. Callers decide whether a truncated body is an
4
+ * error and retain the response status when they need provider-error mapping.
5
+ */
6
+ export interface BoundedResponseBody {
7
+ bytes: Uint8Array<ArrayBuffer>;
8
+ truncated: boolean;
9
+ }
10
+ export interface BoundedResponseOptions {
11
+ /** Error returned when the body signal aborts. */
12
+ abortError?: () => Error;
13
+ /** Keep a stream reader locked until its asynchronous cancellation settles. */
14
+ deferReaderRelease?: boolean;
15
+ }
16
+ /**
17
+ * Reject a caller-facing operation as soon as a signal aborts. The underlying
18
+ * operation is deliberately not awaited after that point: browser/network
19
+ * implementations are allowed to ignore AbortSignal, but a bounded SDK API
20
+ * must still settle at its deadline. Its promise remains observed so a late
21
+ * rejection cannot become an unhandled rejection.
22
+ */
23
+ export declare function raceWithAbort<T>(operation: PromiseLike<T>, signal: AbortSignal, onAbort: () => void, error: () => Error): Promise<T>;
24
+ export declare function readBoundedResponseBody(response: Response, maxBytes: number, signal?: AbortSignal, options?: BoundedResponseOptions): Promise<BoundedResponseBody>;
@@ -0,0 +1,199 @@
1
+ function abortError() {
2
+ return new DOMException("The operation was aborted", "AbortError");
3
+ }
4
+ /**
5
+ * Reject a caller-facing operation as soon as a signal aborts. The underlying
6
+ * operation is deliberately not awaited after that point: browser/network
7
+ * implementations are allowed to ignore AbortSignal, but a bounded SDK API
8
+ * must still settle at its deadline. Its promise remains observed so a late
9
+ * rejection cannot become an unhandled rejection.
10
+ */
11
+ export function raceWithAbort(operation, signal, onAbort, error) {
12
+ if (signal.aborted) {
13
+ try {
14
+ onAbort();
15
+ }
16
+ catch {
17
+ // Cleanup is best effort; the caller-facing abort error remains stable.
18
+ }
19
+ // The operation may already have been started by the caller. Observe its
20
+ // eventual rejection even though the public operation is already aborted.
21
+ void Promise.resolve(operation).catch(() => undefined);
22
+ return Promise.reject(error());
23
+ }
24
+ return new Promise((resolve, reject) => {
25
+ let settled = false;
26
+ const cleanup = () => signal.removeEventListener("abort", abort);
27
+ const finish = (callback) => {
28
+ if (settled)
29
+ return;
30
+ settled = true;
31
+ cleanup();
32
+ callback();
33
+ };
34
+ const abort = () => {
35
+ // A resolved fetch can still have its abort event delivered first when a
36
+ // caller aborts immediately after starting it. Let an already-resolved
37
+ // operation hand its response to the bounded body reader, which can then
38
+ // cancel that reader instead of losing it in the fetch race.
39
+ queueMicrotask(() => {
40
+ finish(() => {
41
+ try {
42
+ onAbort();
43
+ }
44
+ catch {
45
+ // Cleanup is best effort; the caller-facing abort error remains stable.
46
+ }
47
+ reject(error());
48
+ });
49
+ });
50
+ };
51
+ signal.addEventListener("abort", abort, { once: true });
52
+ Promise.resolve(operation).then((value) => finish(() => resolve(value)), (reason) => finish(() => reject(reason)));
53
+ if (signal.aborted)
54
+ abort();
55
+ });
56
+ }
57
+ function cancelWithoutWaiting(cancel) {
58
+ try {
59
+ return Promise.resolve(cancel()).catch(() => undefined);
60
+ }
61
+ catch {
62
+ // A stream may reject cancellation synchronously; the caller is already
63
+ // rejecting or returning a bounded result, so cleanup remains best effort.
64
+ return Promise.resolve();
65
+ }
66
+ }
67
+ function releaseReaderWithoutThrowing(reader) {
68
+ try {
69
+ reader.releaseLock();
70
+ }
71
+ catch {
72
+ // A reader with a still-pending read can reject lock release. The bounded
73
+ // caller must retain its abort/size result rather than waiting for or
74
+ // replacing it with stream cleanup failure.
75
+ }
76
+ }
77
+ export async function readBoundedResponseBody(response, maxBytes, signal, options = {}) {
78
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
79
+ throw new Error("invalid response body limit");
80
+ }
81
+ const cancelBody = () => {
82
+ if (response.body)
83
+ cancelWithoutWaiting(() => response.body.cancel());
84
+ };
85
+ const cancelAbortedResponse = () => {
86
+ if (!response.body) {
87
+ cancelBody();
88
+ return;
89
+ }
90
+ let reader;
91
+ try {
92
+ reader = response.body.getReader();
93
+ }
94
+ catch {
95
+ cancelBody();
96
+ return;
97
+ }
98
+ const cancellation = cancelWithoutWaiting(() => reader.cancel());
99
+ if (options.deferReaderRelease)
100
+ void cancellation.then(() => releaseReaderWithoutThrowing(reader));
101
+ else
102
+ releaseReaderWithoutThrowing(reader);
103
+ };
104
+ if (signal?.aborted) {
105
+ cancelAbortedResponse();
106
+ throw options.abortError?.() ?? abortError();
107
+ }
108
+ const contentLength = response.headers.get("content-length");
109
+ if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > maxBytes)) {
110
+ cancelBody();
111
+ return { bytes: new Uint8Array(), truncated: true };
112
+ }
113
+ if (!response.body)
114
+ return { bytes: new Uint8Array(), truncated: false };
115
+ const reader = response.body.getReader();
116
+ let cancellation;
117
+ const cancelReader = () => {
118
+ cancellation ??= cancelWithoutWaiting(() => reader.cancel());
119
+ };
120
+ const releaseReader = () => {
121
+ if (options.deferReaderRelease && cancellation) {
122
+ void cancellation.then(() => releaseReaderWithoutThrowing(reader));
123
+ }
124
+ else {
125
+ releaseReaderWithoutThrowing(reader);
126
+ }
127
+ };
128
+ const chunks = [];
129
+ let bytesRead = 0;
130
+ const readChunk = () => {
131
+ if (!signal)
132
+ return reader.read();
133
+ if (signal.aborted) {
134
+ cancelReader();
135
+ return Promise.reject(options.abortError?.() ?? abortError());
136
+ }
137
+ return new Promise((resolve, reject) => {
138
+ let settled = false;
139
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
140
+ const onAbort = () => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ cancelReader();
145
+ cleanup();
146
+ reject(options.abortError?.() ?? abortError());
147
+ };
148
+ signal.addEventListener("abort", onAbort, { once: true });
149
+ reader.read().then((result) => {
150
+ if (settled)
151
+ return;
152
+ settled = true;
153
+ cleanup();
154
+ resolve(result);
155
+ }, (error) => {
156
+ if (settled)
157
+ return;
158
+ settled = true;
159
+ cleanup();
160
+ reject(error);
161
+ });
162
+ });
163
+ };
164
+ try {
165
+ for (;;) {
166
+ const { done, value } = await readChunk();
167
+ if (done)
168
+ break;
169
+ const remaining = maxBytes - bytesRead;
170
+ if (value.byteLength > remaining) {
171
+ if (remaining > 0) {
172
+ const chunk = new Uint8Array(remaining);
173
+ chunk.set(value.subarray(0, remaining));
174
+ chunks.push(chunk);
175
+ bytesRead += remaining;
176
+ }
177
+ cancelReader();
178
+ return { bytes: joinBytes(chunks, bytesRead), truncated: true };
179
+ }
180
+ const chunk = new Uint8Array(value.byteLength);
181
+ chunk.set(value);
182
+ chunks.push(chunk);
183
+ bytesRead += value.byteLength;
184
+ }
185
+ }
186
+ finally {
187
+ releaseReader();
188
+ }
189
+ return { bytes: joinBytes(chunks, bytesRead), truncated: false };
190
+ }
191
+ function joinBytes(chunks, total) {
192
+ const bytes = new Uint8Array(total);
193
+ let offset = 0;
194
+ for (const chunk of chunks) {
195
+ bytes.set(chunk, offset);
196
+ offset += chunk.byteLength;
197
+ }
198
+ return bytes;
199
+ }
@@ -3,3 +3,4 @@ export * from "./types.js";
3
3
  export * from "./operations.js";
4
4
  export * from "./errors.js";
5
5
  export * from "./oidc.js";
6
+ export * from "./constants.js";
@@ -3,3 +3,4 @@ export * from "./types.js";
3
3
  export * from "./operations.js";
4
4
  export * from "./errors.js";
5
5
  export * from "./oidc.js";
6
+ export * from "./constants.js";
@@ -1,69 +1,63 @@
1
- import { type OidcClientConfig, type OidcRegistrationClientMetadata, type DeviceAuthorizationResponse, type DeviceAccessTokenResponse, type DeviceAccessTokenError, type BearerTokenResponse } from "matrix-js-sdk/lib/oidc/index.js";
1
+ import { type DeviceAuthorizationResponse, type DeviceAccessTokenResponse, type DeviceAccessTokenError, type BearerTokenResponse as OAuthBearerTokenResponse, type ValidatedAuthMetadata } from "matrix-js-sdk/lib/oauth/index.js";
2
2
  import type { AccessTokens, TokenRefreshFunction } from "matrix-js-sdk/lib/http-api/index.js";
3
- import type { IdTokenClaims } from "oidc-client-ts";
4
- export type { OidcClientConfig, DeviceAuthorizationResponse, DeviceAccessTokenResponse, DeviceAccessTokenError, BearerTokenResponse, AccessTokens, TokenRefreshFunction, };
5
- /**
6
- * The full result of a completed OIDC login (device-code or authorization
7
- * code) — enough for a caller to build a ready `TeleCryptIOStorage` via
8
- * `TeleCryptIOStorage.createFromOidc(...)` AND to persist for reuse/refresh
9
- * across restarts (CLI: profile file; UI: localStorage).
10
- */
11
- export interface OidcTokenSet {
12
- homeserverUrl: string;
13
- issuer: string;
14
- clientId: string;
15
- userId: string;
16
- deviceId: string;
17
- accessToken: string;
18
- refreshToken?: string;
19
- idToken: string;
20
- /** epoch ms, if the token response included an expiry. */
21
- expiresAt?: number;
3
+ /** Matrix 1.18 OAuth metadata returned by `/auth_metadata`. */
4
+ export type OidcClientConfig = Omit<ValidatedAuthMetadata, "revocation_endpoint"> & {
5
+ revocation_endpoint?: string;
6
+ };
7
+ export type { DeviceAuthorizationResponse, DeviceAccessTokenResponse, DeviceAccessTokenError, AccessTokens, TokenRefreshFunction };
8
+ /** The bearer response retained by the browser authorization-code API. */
9
+ export type BearerTokenResponse = OAuthBearerTokenResponse & {
10
+ scope: string;
11
+ };
12
+ /** The camel-case registration contract used by TeleCrypt web and CLI callers. */
13
+ export interface OidcRegistrationClientMetadata {
14
+ clientName?: string;
15
+ clientUri: string;
16
+ logoUri?: string;
17
+ applicationType?: "web" | "native";
18
+ redirectUris?: [string, ...string[]];
19
+ contacts?: string[];
20
+ tosUri?: string;
21
+ policyUri?: string;
22
22
  }
23
+ /** The issuer-bound endpoints accepted by the token refresh adapter. */
24
+ export type OidcTokenEndpointMetadata = Pick<OidcClientConfig, "issuer" | "token_endpoint" | "revocation_endpoint">;
23
25
  /**
24
26
  * Discovers the OIDC/MAS issuer and endpoints for a homeserver via the
25
- * Matrix client's authentication metadata endpoint.
26
- *
27
- * NODE CALLERS: this internally constructs oidc-client-ts state that
28
- * requires `window.sessionStorage`/`window.localStorage` to exist (a real
29
- * browser has both natively). A Node adapter may install those only around
30
- * this call: a permanent global `window` breaks rust-crypto WASM's own
31
- * environment detection. The functional-test-only adapter is kept in
32
- * `test/harness/oidcWindowShim.ts`, rather than in the production SDK.
27
+ * Matrix client's stable `/auth_metadata` endpoint. Matrix 42 performs this
28
+ * as a plain metadata fetch; discovery is safe for browser and Node callers.
33
29
  */
34
- export declare function discoverOidcIssuer(homeserverBaseUrl: string): Promise<OidcClientConfig>;
30
+ export declare function discoverOidcIssuer(homeserverBaseUrl: string, signal?: AbortSignal): Promise<OidcClientConfig>;
35
31
  /**
36
32
  * Dynamic client registration (DCR). `clientUri`/`redirectUris` must share a
37
33
  * host unless the issuer's policy allows a mismatch (our local dev/test MAS
38
34
  * does; a production issuer may not).
39
35
  */
40
- export declare function registerClient(authMetadata: OidcClientConfig, metadata: OidcRegistrationClientMetadata): Promise<string>;
36
+ export declare function registerClient(authMetadata: OidcClientConfig, metadata: OidcRegistrationClientMetadata, signal?: AbortSignal): Promise<string>;
41
37
  /**
42
38
  * Starts a device-code authorization session. `deviceId` is caller-chosen
43
39
  * (unlike the authorization-code flow, where the SDK picks a random one) so
44
40
  * it can be reused as this Matrix device's `device_id` throughout —
45
- * `generateScope(deviceId)` embeds it in the requested scope per MSC2967.
41
+ * `generateScope(deviceId)` embeds it in the requested stable Matrix OAuth
42
+ * scope.
46
43
  */
47
- export declare function startDeviceCodeLogin(authMetadata: OidcClientConfig, clientId: string, deviceId: string): Promise<DeviceAuthorizationResponse>;
44
+ export declare function startDeviceCodeLogin(authMetadata: OidcClientConfig, clientId: string, deviceId: string, signal?: AbortSignal): Promise<DeviceAuthorizationResponse>;
48
45
  /**
49
46
  * Polls the token endpoint until the device-code session is approved (or
50
47
  * denied/expired) — see matrix-js-sdk's `waitForDeviceAuthorization` for the
51
48
  * RFC 8628 polling semantics (honours `interval`/`expires_in`,
52
- * `authorization_pending`/`slow_down`).
49
+ * `authorization_pending`/`slow_down`). Sessions returned by
50
+ * startDeviceCodeLogin carry their binding in memory; reconstructed sessions
51
+ * must provide expectedDeviceId and are rejected before polling when no
52
+ * binding is available.
53
53
  */
54
- export declare function waitForDeviceCodeLogin(authMetadata: OidcClientConfig, clientId: string, session: DeviceAuthorizationResponse): Promise<DeviceAccessTokenResponse | DeviceAccessTokenError>;
54
+ export declare function waitForDeviceCodeLogin(authMetadata: OidcClientConfig, clientId: string, session: DeviceAuthorizationResponse, signal?: AbortSignal, expectedDeviceId?: string): Promise<DeviceAccessTokenResponse | DeviceAccessTokenError>;
55
55
  export declare function isDeviceAccessTokenError(result: DeviceAccessTokenResponse | DeviceAccessTokenError): result is DeviceAccessTokenError;
56
56
  /**
57
- * Builds the authorization URL to redirect the browser to. PKCE
58
- * verifier/state/nonce are generated and persisted to `window.sessionStorage`
59
- * internally by matrix-js-sdk/oidc-client-ts (`mx_oidc_`-prefixed keys) — the
60
- * caller does not need to manage them; `completeAuthorizationCodeFlow` reads
61
- * them back after the redirect.
62
- *
63
- * `deviceId` is optional: when supplied, it is embedded in the requested
64
- * scope (MSC2967) so MAS grants THAT device instead of minting a fresh one —
65
- * the web UI passes a stable per-browser device id so repeated logins reuse
66
- * the same Matrix device instead of accumulating one device per login.
57
+ * Builds the authorization URL to redirect the browser to. The Matrix 42
58
+ * OAuth2 helper owns PKCE challenge generation and URL construction; this
59
+ * SDK owns only the minimal context needed to recreate that helper after the
60
+ * redirect. The context is keyed by a one-time random state in sessionStorage.
67
61
  */
68
62
  export declare function beginAuthorizationCodeFlow(opts: {
69
63
  authMetadata: OidcClientConfig;
@@ -71,52 +65,36 @@ export declare function beginAuthorizationCodeFlow(opts: {
71
65
  homeserverUrl: string;
72
66
  redirectUri: string;
73
67
  deviceId?: string;
68
+ signal?: AbortSignal;
74
69
  }): Promise<string>;
75
70
  /** Completes the authorization-code exchange after the `?code&state` redirect. */
76
- export declare function completeAuthorizationCodeFlow(code: string, state: string): Promise<{
71
+ export declare function completeAuthorizationCodeFlow(code: string, state: string, signal?: AbortSignal): Promise<{
77
72
  tokenResponse: BearerTokenResponse;
78
73
  oidcClientSettings: {
79
74
  clientId: string;
80
75
  issuer: string;
81
76
  };
82
77
  homeserverUrl: string;
83
- idTokenClaims: IdTokenClaims;
84
78
  }>;
85
- /** The device_id granted is embedded in the token response's `scope`
86
- * (`urn:matrix:org.matrix.msc2967.client:device:<id>`), per MSC2967 — the
87
- * authorization-code flow doesn't return it any other way (the SDK
88
- * auto-generates it when the caller doesn't request a specific one). */
79
+ /** The device_id granted is embedded in the token response's stable Matrix
80
+ * OAuth scope. */
89
81
  export declare function extractDeviceIdFromScope(scope: string): string | null;
90
82
  /** Confirms {userId, deviceId} for a freshly-obtained access token via
91
83
  * `GET /_matrix/client/v3/account/whoami` — needed because neither the
92
84
  * device-code nor authorization-code token response includes the Matrix
93
85
  * user ID. */
94
- export declare function whoAmI(homeserverUrl: string, accessToken: string): Promise<{
86
+ export declare function whoAmI(homeserverUrl: string, accessToken: string, serverName: string, signal?: AbortSignal): Promise<{
95
87
  userId: string;
96
88
  deviceId: string | null;
97
89
  }>;
98
- /**
99
- * Refreshes an access token via a plain RFC 6749 `grant_type=refresh_token`
100
- * POST to the token endpoint — deliberately NOT matrix-js-sdk's
101
- * `OidcTokenRefresher`/oidc-client-ts, which unconditionally construct an
102
- * internal `OidcClient` requiring `window.sessionStorage`/`window.
103
- * localStorage` even though a plain refresh never actually reads or writes
104
- * them (confirmed by direct testing: `ReferenceError: window is not
105
- * defined` under plain Node; see test/functional/oidc.test.ts O.2). A public client (`token_endpoint_auth_method:
106
- * "none"`, what `registerClient` above registers) authenticates a refresh
107
- * with just `client_id` in the body, no secret — this is the whole request.
108
- * Works identically in Node and the browser (plain `fetch`), so both
109
- * adapters share it with zero platform-specific code.
110
- */
111
- export declare function refreshOidcToken(tokenEndpoint: string, clientId: string, refreshToken: string): Promise<AccessTokens>;
112
90
  /**
113
91
  * Builds the `tokenRefreshFunction` `TeleCryptIOStorage.createFromOidc`
114
- * expects: refreshes via `refreshOidcToken`, then calls `onPersist` with the
115
- * new tokens before returning them — so a caller's adapter-specific
92
+ * expects: refreshes via the internal OAuth request, then calls `onPersist`
93
+ * with the new tokens before returning them — so a caller's adapter-specific
116
94
  * persistence (CLI: profile file; UI: localStorage) always sees a refresh
117
95
  * that just happened, not just ones it initiated itself.
118
96
  */
119
- export declare function buildTokenRefreshFunction(tokenEndpoint: string, clientId: string, onPersist: (tokens: {
97
+ export declare function buildTokenRefreshFunction(metadata: OidcTokenEndpointMetadata, clientId: string, onPersist: (tokens: {
120
98
  accessToken: string;
121
99
  refreshToken?: string;
122
- }) => Promise<void>): TokenRefreshFunction;
100
+ }, signal?: AbortSignal) => Promise<void>, expectedDeviceId: string): TokenRefreshFunction & ((refreshToken: string, signal?: AbortSignal) => Promise<AccessTokens>);