@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 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 || tokenResponse.scope !== generateScope(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 !== expectedScope) {
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("urn:matrix:client:device:"));
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 deviceId = deviceTokens[0].slice("urn:matrix:client:device:".length);
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 (extractDeviceIdFromScope(grantedScope) === null ||
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
  }
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",