@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.
- package/README.md +54 -9
- package/dist/TeleCryptIOStorage.d.ts +86 -45
- package/dist/TeleCryptIOStorage.js +1418 -230
- package/dist/core/constants.d.ts +16 -0
- package/dist/core/constants.js +54 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +108 -0
- package/dist/core/http.d.ts +24 -0
- package/dist/core/http.js +199 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.d.ts +47 -69
- package/dist/core/oidc.js +1196 -137
- package/dist/core/operations.d.ts +31 -32
- package/dist/core/operations.js +1270 -219
- package/dist/core/poll.d.ts +2 -1
- package/dist/core/poll.js +59 -3
- package/dist/core/types.d.ts +14 -0
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +11 -9
|
@@ -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
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/errors.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/dist/core/oidc.d.ts
CHANGED
|
@@ -1,69 +1,63 @@
|
|
|
1
|
-
import { type
|
|
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
|
-
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
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.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
|
86
|
-
*
|
|
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
|
|
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(
|
|
97
|
+
export declare function buildTokenRefreshFunction(metadata: OidcTokenEndpointMetadata, clientId: string, onPersist: (tokens: {
|
|
120
98
|
accessToken: string;
|
|
121
99
|
refreshToken?: string;
|
|
122
|
-
}) => Promise<void
|
|
100
|
+
}, signal?: AbortSignal) => Promise<void>, expectedDeviceId: string): TokenRefreshFunction & ((refreshToken: string, signal?: AbortSignal) => Promise<AccessTokens>);
|