@telecrypt-io/storage 0.5.25 → 0.5.27
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/dist/core/oidc.js +50 -7
- package/dist/core/operations.js +22 -1
- package/package.json +1 -1
package/dist/core/oidc.js
CHANGED
|
@@ -35,6 +35,9 @@ const MAX_OIDC_TOKEN_LENGTH = 8192;
|
|
|
35
35
|
const MAX_OIDC_SCOPE_LENGTH = 4096;
|
|
36
36
|
const MAX_OIDC_CONTACTS = 16;
|
|
37
37
|
const MAX_OIDC_REDIRECT_URIS = 8;
|
|
38
|
+
const LEGACY_MATRIX_SCOPE_PREFIX = "urn:matrix:client:";
|
|
39
|
+
const MSC2967_MATRIX_SCOPE_PREFIX = "urn:matrix:org.matrix.msc2967.client:";
|
|
40
|
+
const MATRIX_SCOPE_PREFIXES = [LEGACY_MATRIX_SCOPE_PREFIX, MSC2967_MATRIX_SCOPE_PREFIX];
|
|
38
41
|
// MAS advertises a 20-minute device-code lifetime. Keep that provider window
|
|
39
42
|
// as the hard upper bound while retaining a finite polling budget.
|
|
40
43
|
const MAX_DEVICE_EXPIRES_IN_SECONDS = 20 * 60;
|
|
@@ -791,7 +794,9 @@ export async function waitForDeviceCodeLogin(authMetadata, clientId, session, si
|
|
|
791
794
|
}, Math.min(OIDC_REQUEST_TIMEOUT_MS, remaining), signal);
|
|
792
795
|
if (response.ok && isValidDeviceAccessTokenResponse(body)) {
|
|
793
796
|
const tokenResponse = validateDeviceAccessTokenResponse(body);
|
|
794
|
-
if (!requestedDeviceId ||
|
|
797
|
+
if (!requestedDeviceId ||
|
|
798
|
+
typeof tokenResponse.scope !== "string" ||
|
|
799
|
+
!scopeMatchesDevice(tokenResponse.scope, requestedDeviceId)) {
|
|
795
800
|
throw new OidcValidationError("OIDC device authorization returned an unexpected granted scope");
|
|
796
801
|
}
|
|
797
802
|
ensureOidcNotCancelled(signal, "device authorization");
|
|
@@ -940,9 +945,8 @@ export async function completeAuthorizationCodeFlow(code, state, signal) {
|
|
|
940
945
|
validateBearerTokenResponse(body);
|
|
941
946
|
validateTokenBounds(body, "authorization code exchange");
|
|
942
947
|
const normalized = normalizeBearerTokenResponseTokenType(body);
|
|
943
|
-
const expectedScope = generateScope(context.deviceId);
|
|
944
948
|
const grantedScope = normalized.scope;
|
|
945
|
-
if (typeof grantedScope !== "string" || grantedScope
|
|
949
|
+
if (typeof grantedScope !== "string" || !scopeMatchesDevice(grantedScope, context.deviceId)) {
|
|
946
950
|
throw new StorageError("OIDC authorization code exchange returned an unexpected granted scope");
|
|
947
951
|
}
|
|
948
952
|
const tokenResponse = {
|
|
@@ -975,12 +979,52 @@ export function extractDeviceIdFromScope(scope) {
|
|
|
975
979
|
return null;
|
|
976
980
|
}
|
|
977
981
|
const tokens = scope.split(/\s+/);
|
|
978
|
-
const deviceTokens = tokens.filter((token) => token.startsWith(
|
|
982
|
+
const deviceTokens = tokens.filter((token) => MATRIX_SCOPE_PREFIXES.some((prefix) => token.startsWith(`${prefix}device:`)));
|
|
979
983
|
if (deviceTokens.length !== 1)
|
|
980
984
|
return null;
|
|
981
|
-
const
|
|
985
|
+
const deviceToken = deviceTokens[0];
|
|
986
|
+
const prefix = MATRIX_SCOPE_PREFIXES.find((candidate) => deviceToken.startsWith(`${candidate}device:`));
|
|
987
|
+
if (!prefix)
|
|
988
|
+
return null;
|
|
989
|
+
const deviceId = deviceToken.slice(`${prefix}device:`.length);
|
|
982
990
|
return DEVICE_ID_PATTERN.test(deviceId) ? deviceId : null;
|
|
983
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Accepts the legacy Matrix OAuth scope and the current MSC2967 namespace
|
|
994
|
+
* returned by MAS. The provider may add the standard `openid` scope, but the
|
|
995
|
+
* API and device grants must remain one exact namespace pair bound to the
|
|
996
|
+
* device that this client requested.
|
|
997
|
+
*/
|
|
998
|
+
function scopeMatchesDevice(scope, expectedDeviceId) {
|
|
999
|
+
if (typeof scope !== "string" ||
|
|
1000
|
+
typeof expectedDeviceId !== "string" ||
|
|
1001
|
+
!DEVICE_ID_PATTERN.test(expectedDeviceId) ||
|
|
1002
|
+
scope.length === 0 ||
|
|
1003
|
+
scope.length > MAX_OIDC_SCOPE_LENGTH ||
|
|
1004
|
+
/[\u0000-\u001f\u007f]/.test(scope) ||
|
|
1005
|
+
scope.trim() !== scope ||
|
|
1006
|
+
/\s{2,}/.test(scope)) {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
const tokens = scope.split(/\s+/);
|
|
1010
|
+
if (new Set(tokens).size !== tokens.length)
|
|
1011
|
+
return false;
|
|
1012
|
+
return MATRIX_SCOPE_PREFIXES.some((prefix) => {
|
|
1013
|
+
const expected = new Set([
|
|
1014
|
+
`${prefix}api:*`,
|
|
1015
|
+
`${prefix}device:${expectedDeviceId}`,
|
|
1016
|
+
"openid",
|
|
1017
|
+
]);
|
|
1018
|
+
const required = new Set([
|
|
1019
|
+
`${prefix}api:*`,
|
|
1020
|
+
`${prefix}device:${expectedDeviceId}`,
|
|
1021
|
+
]);
|
|
1022
|
+
return (tokens.length >= required.size &&
|
|
1023
|
+
tokens.length <= expected.size &&
|
|
1024
|
+
tokens.every((token) => expected.has(token)) &&
|
|
1025
|
+
[...required].every((token) => tokens.includes(token)));
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
984
1028
|
function validateMatrixUserId(userId, serverName) {
|
|
985
1029
|
try {
|
|
986
1030
|
return validateCanonicalMatrixUserId(userId, serverName);
|
|
@@ -1120,8 +1164,7 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1120
1164
|
catch {
|
|
1121
1165
|
throw new StorageError("OIDC token refresh returned an invalid scope");
|
|
1122
1166
|
}
|
|
1123
|
-
if (
|
|
1124
|
-
grantedScope !== generateScope(expectedDeviceId)) {
|
|
1167
|
+
if (!scopeMatchesDevice(grantedScope, expectedDeviceId)) {
|
|
1125
1168
|
throw new StorageError("OIDC token refresh returned an unexpected granted scope");
|
|
1126
1169
|
}
|
|
1127
1170
|
}
|
package/dist/core/operations.js
CHANGED
|
@@ -248,7 +248,8 @@ function assertTreeEmptyForDeletion(storage, tree, spaces) {
|
|
|
248
248
|
try {
|
|
249
249
|
files = tree
|
|
250
250
|
.listAllFiles()
|
|
251
|
-
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id)
|
|
251
|
+
.filter((file) => !isMarkedFileDeleted(storage, tree.id, file.id) &&
|
|
252
|
+
!isConfirmedDeletedFile(storage.getClient(), tree.id, file));
|
|
252
253
|
}
|
|
253
254
|
catch {
|
|
254
255
|
throw new StorageError("could not enumerate storage files safely");
|
|
@@ -333,6 +334,26 @@ function isActiveRelationEvent(event) {
|
|
|
333
334
|
!Array.isArray(content) &&
|
|
334
335
|
Object.keys(content).length > 0);
|
|
335
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* A deleted MSC3089 branch is represented by an authoritative empty state
|
|
339
|
+
* event. Inactive branches with metadata are historical versions, and a
|
|
340
|
+
* missing or malformed state cannot prove that the file was deleted.
|
|
341
|
+
*/
|
|
342
|
+
function isConfirmedDeletedFile(client, treeId, file) {
|
|
343
|
+
if (file.isActive)
|
|
344
|
+
return false;
|
|
345
|
+
const stateEvents = readRelationEvents(client, treeId, UNSTABLE_MSC3089_BRANCH.name, file.id);
|
|
346
|
+
if (!stateEvents || stateEvents.length !== 1)
|
|
347
|
+
return false;
|
|
348
|
+
const stateEvent = stateEvents[0];
|
|
349
|
+
if (relationStateKey(stateEvent) !== file.id)
|
|
350
|
+
return false;
|
|
351
|
+
const content = stateEvent.getContent?.();
|
|
352
|
+
return (typeof content === "object" &&
|
|
353
|
+
content !== null &&
|
|
354
|
+
!Array.isArray(content) &&
|
|
355
|
+
Object.keys(content).length === 0);
|
|
356
|
+
}
|
|
336
357
|
/**
|
|
337
358
|
* Deletion is intentionally narrower than MSC3089TreeSpace.delete(): a local
|
|
338
359
|
* child graph is not proof that the rooms are exclusively owned by this tree.
|