@awiki/dsh-plugin 0.3.17 → 0.3.19

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 (107) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +11 -0
  3. package/THIRD_PARTY_NOTICES.md +6 -0
  4. package/lib/client.js +4012 -671
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +603 -61
  7. package/lib/{sdk-adapter-DGcTiEZz.mjs → package-version.generated-nZcAVnm8.mjs} +103 -10
  8. package/lib/provider.js +5 -3
  9. package/lib/typert.host.js +553 -133
  10. package/lib/typert.remote-client.d.ts +11 -3
  11. package/lib/typert.remote-client.d.ts.map +1 -1
  12. package/lib/typert.remote-client.js +553 -133
  13. package/lib/types/backend-capabilities.d.ts +12 -0
  14. package/lib/types/backend-capabilities.d.ts.map +1 -0
  15. package/lib/types/backend-capabilities.js +48 -0
  16. package/lib/types/backend-capabilities.js.map +1 -0
  17. package/lib/types/client/AwikiAvatar.d.ts +40 -0
  18. package/lib/types/client/AwikiAvatar.d.ts.map +1 -0
  19. package/lib/types/client/AwikiAvatar.js +201 -0
  20. package/lib/types/client/AwikiAvatar.js.map +1 -0
  21. package/lib/types/client/AwikiAvatarEditor.d.ts +11 -0
  22. package/lib/types/client/AwikiAvatarEditor.d.ts.map +1 -0
  23. package/lib/types/client/AwikiAvatarEditor.js +113 -0
  24. package/lib/types/client/AwikiAvatarEditor.js.map +1 -0
  25. package/lib/types/client/AwikiAvatarPreview.d.ts +8 -0
  26. package/lib/types/client/AwikiAvatarPreview.d.ts.map +1 -0
  27. package/lib/types/client/AwikiAvatarPreview.js +68 -0
  28. package/lib/types/client/AwikiAvatarPreview.js.map +1 -0
  29. package/lib/types/client/AwikiGroupDetails.js +2 -2
  30. package/lib/types/client/AwikiGroupDetails.js.map +1 -1
  31. package/lib/types/client/AwikiIdentityAccess.d.ts +1 -0
  32. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  33. package/lib/types/client/AwikiIdentityAccess.js +21 -4
  34. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  35. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  36. package/lib/types/client/AwikiOverlay.js +97 -29
  37. package/lib/types/client/AwikiOverlay.js.map +1 -1
  38. package/lib/types/client/AwikiProfileCard.d.ts +1 -1
  39. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
  40. package/lib/types/client/AwikiProfileCard.js +6 -2
  41. package/lib/types/client/AwikiProfileCard.js.map +1 -1
  42. package/lib/types/client/AwikiSettingsSection.d.ts +1 -0
  43. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  44. package/lib/types/client/AwikiSettingsSection.js +5 -4
  45. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  46. package/lib/types/client/avatar-cache.d.ts +24 -0
  47. package/lib/types/client/avatar-cache.d.ts.map +1 -0
  48. package/lib/types/client/avatar-cache.js +336 -0
  49. package/lib/types/client/avatar-cache.js.map +1 -0
  50. package/lib/types/client/avatar-dialog.d.ts +3 -0
  51. package/lib/types/client/avatar-dialog.d.ts.map +1 -0
  52. package/lib/types/client/avatar-dialog.js +18 -0
  53. package/lib/types/client/avatar-dialog.js.map +1 -0
  54. package/lib/types/client/avatar-image.d.ts +26 -0
  55. package/lib/types/client/avatar-image.d.ts.map +1 -0
  56. package/lib/types/client/avatar-image.js +193 -0
  57. package/lib/types/client/avatar-image.js.map +1 -0
  58. package/lib/types/client/controller.d.ts +37 -4
  59. package/lib/types/client/controller.d.ts.map +1 -1
  60. package/lib/types/client/controller.js +306 -59
  61. package/lib/types/client/controller.js.map +1 -1
  62. package/lib/types/client/index.d.ts.map +1 -1
  63. package/lib/types/client/index.js +11 -2
  64. package/lib/types/client/index.js.map +1 -1
  65. package/lib/types/client/settings-locales.d.ts +1 -1
  66. package/lib/types/client/settings-locales.d.ts.map +1 -1
  67. package/lib/types/client/settings-locales.js +26 -2
  68. package/lib/types/client/settings-locales.js.map +1 -1
  69. package/lib/types/client/slots.d.ts +12 -3
  70. package/lib/types/client/slots.d.ts.map +1 -1
  71. package/lib/types/conversation-preferences.d.ts.map +1 -1
  72. package/lib/types/conversation-preferences.js +27 -0
  73. package/lib/types/conversation-preferences.js.map +1 -1
  74. package/lib/types/index.d.ts +26 -3
  75. package/lib/types/index.d.ts.map +1 -1
  76. package/lib/types/index.js +321 -33
  77. package/lib/types/index.js.map +1 -1
  78. package/lib/types/listener.d.ts.map +1 -1
  79. package/lib/types/listener.js +9 -8
  80. package/lib/types/listener.js.map +1 -1
  81. package/lib/types/package-version.generated.d.ts +2 -2
  82. package/lib/types/package-version.generated.js +2 -2
  83. package/lib/types/provider-api.d.ts +28 -3
  84. package/lib/types/provider-api.d.ts.map +1 -1
  85. package/lib/types/provider.d.ts.map +1 -1
  86. package/lib/types/provider.js +5 -2
  87. package/lib/types/provider.js.map +1 -1
  88. package/lib/types/sdk-adapter.d.ts +10 -3
  89. package/lib/types/sdk-adapter.d.ts.map +1 -1
  90. package/lib/types/sdk-adapter.js +78 -7
  91. package/lib/types/sdk-adapter.js.map +1 -1
  92. package/lib/types/tenant-capability-cache.d.ts +15 -0
  93. package/lib/types/tenant-capability-cache.d.ts.map +1 -0
  94. package/lib/types/tenant-capability-cache.js +45 -0
  95. package/lib/types/tenant-capability-cache.js.map +1 -0
  96. package/lib/types/tenant-registry.d.ts +2 -0
  97. package/lib/types/tenant-registry.d.ts.map +1 -1
  98. package/lib/types/tenant-registry.js +83 -16
  99. package/lib/types/tenant-registry.js.map +1 -1
  100. package/lib/types/types.d.ts +70 -7
  101. package/lib/types/types.d.ts.map +1 -1
  102. package/lib/types/types.js.map +1 -1
  103. package/lib/types/update-policy.d.ts +2 -2
  104. package/lib/types/update-policy.d.ts.map +1 -1
  105. package/lib/types/update-policy.js +10 -1
  106. package/lib/types/update-policy.js.map +1 -1
  107. package/package.json +33 -34
@@ -32,6 +32,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
32
32
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
33
  done = true;
34
34
  };
35
+ import { TenantCapabilityCache } from "./tenant-capability-cache.js";
36
+ import { backendAttachmentMaxBytes, projectBackendFeatures, projectBackendSnapshot, requireBackendFeature } from "./backend-capabilities.js";
35
37
  export { registerAwikiLoopbackRpc } from "./settings-transport.js";
36
38
  import { registerAwikiSettingsTransport } from "./settings-transport.js";
37
39
  import { registrationHandleLocalPart } from "./registration-policy.js";
@@ -115,6 +117,8 @@ export const Config = z.object({
115
117
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES),
116
118
  });
117
119
  const FAILURE_CODES = new Set([
120
+ 'backend-not-supported',
121
+ 'backend-unconfirmed',
118
122
  'client-version-unsupported',
119
123
  'not-registered',
120
124
  'signed-out',
@@ -132,6 +136,7 @@ const FAILURE_CODES = new Set([
132
136
  'state-in-use',
133
137
  'rate-limited',
134
138
  'group-membership-required',
139
+ 'group-member-encryption-not-ready',
135
140
  'group-identity-stale',
136
141
  'attachment-too-large',
137
142
  'summary-unavailable',
@@ -154,6 +159,8 @@ const RESUMABLE_JOIN_PHASES = new Set([
154
159
  'approval_prepared',
155
160
  ]);
156
161
  const FAILURE_MESSAGES = {
162
+ 'backend-not-supported': 'This server does not support this feature.',
163
+ 'backend-unconfirmed': 'Server features are not confirmed yet. Please retry.',
157
164
  'client-version-unsupported': 'This AWiki plugin version is not supported by the active tenant. Update the plugin or switch tenants.',
158
165
  'not-registered': 'No AWiki identity is registered for this deployment.',
159
166
  'signed-out': 'This installation is signed out of AWiki.',
@@ -170,6 +177,7 @@ const FAILURE_MESSAGES = {
170
177
  'conflict': 'The AWiki operation conflicts with current state.',
171
178
  'state-in-use': 'AWiki data is already open in another client.',
172
179
  'rate-limited': 'The AWiki service rate-limited the request.',
180
+ 'group-member-encryption-not-ready': 'The member has not prepared encrypted messaging.',
173
181
  'group-membership-required': 'The active AWiki identity is not a member of this group.',
174
182
  'group-identity-stale': 'The AWiki group identity binding is still recovering.',
175
183
  'attachment-too-large': 'The attachment exceeds this deployment\'s size limit.',
@@ -272,7 +280,7 @@ function publishedServiceBaseUrl(services, name, allowInsecureLoopbackForTesting
272
280
  if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
273
281
  return undefined;
274
282
  const value = candidate;
275
- if (value.enabled !== true || typeof value.base_url !== 'string')
283
+ if (value.enabled !== true || value.protocol_version !== 1 || typeof value.base_url !== 'string')
276
284
  return undefined;
277
285
  try {
278
286
  const baseUrl = serviceUrl(`server-info.services.${name}.base_url`, value.base_url, allowInsecureLoopbackForTesting);
@@ -470,6 +478,10 @@ const MAX_PROFILE_TAGS = 5;
470
478
  const MAX_PROFILE_TAG_CHARACTERS = 30;
471
479
  const MAX_MESSAGE_CHARACTERS = 20_000;
472
480
  function normalizeCreateGroupRequest(request) {
481
+ if (request?.operationId !== undefined && (typeof request.operationId !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(request.operationId)))
482
+ return undefined;
483
+ if (request?.encrypted !== undefined && typeof request.encrypted !== 'boolean')
484
+ return undefined;
473
485
  if (typeof request?.name !== 'string' || !Array.isArray(request.members))
474
486
  return undefined;
475
487
  const name = request.name.trim();
@@ -490,7 +502,7 @@ function normalizeCreateGroupRequest(request) {
490
502
  seen.add(member);
491
503
  members.push(member);
492
504
  }
493
- return { name, members };
505
+ return { name, members, ...request.operationId === undefined ? {} : { operationId: request.operationId }, ...request.encrypted === undefined ? {} : { encrypted: request.encrypted } };
494
506
  }
495
507
  function normalizeMember(value) {
496
508
  if (typeof value !== 'string')
@@ -505,6 +517,24 @@ function normalizeGroupDid(value) {
505
517
  return undefined;
506
518
  return value;
507
519
  }
520
+ function validAvatarRequest(request, image) {
521
+ if (request === null || typeof request !== 'object' || Array.isArray(request))
522
+ return false;
523
+ const value = request;
524
+ const keys = image ? ['requestId', 'expectedProfileVersion', 'imageBase64'] : ['requestId', 'expectedProfileVersion'];
525
+ if (Object.keys(value).length !== keys.length || Object.keys(value).some(key => !keys.includes(key)))
526
+ return false;
527
+ if (typeof value.requestId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(value.requestId))
528
+ return false;
529
+ if (typeof value.expectedProfileVersion !== 'string' || !/^(0|[1-9][0-9]{0,18})$/u.test(value.expectedProfileVersion) || BigInt(value.expectedProfileVersion) > 9223372036854775807n)
530
+ return false;
531
+ if (!image)
532
+ return true;
533
+ if (typeof value.imageBase64 !== 'string' || value.imageBase64.length === 0 || value.imageBase64.length > 699052)
534
+ return false;
535
+ const bytes = Buffer.from(value.imageBase64, 'base64');
536
+ return bytes.length <= 512 * 1024 && bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[bytes.length - 2] === 255 && bytes[bytes.length - 1] === 217 && bytes.toString('base64') === value.imageBase64;
537
+ }
508
538
  function normalizeProfileRequest(request) {
509
539
  if (typeof request?.displayName !== 'string' || typeof request.bio !== 'string' || !Array.isArray(request.tags))
510
540
  return undefined;
@@ -617,6 +647,8 @@ function identityAccessTarget(request, configuredDomain) {
617
647
  return { localPart, fullHandle: `${localPart}.${configuredDomain}` };
618
648
  }
619
649
  function normalizeSendTextRequest(request) {
650
+ if (request?.security !== undefined && request.security !== 'plain' && request.security !== 'e2ee-required')
651
+ return undefined;
620
652
  if (typeof request?.text !== 'string' || typeof request.idempotencyKey !== 'string'
621
653
  || !isMessageTarget(request.target))
622
654
  return undefined;
@@ -778,6 +810,7 @@ let AwikiService = (() => {
778
810
  let _classSuper = TypertRemoteService;
779
811
  let _instanceExtraInitializers = [];
780
812
  let _getConfig_decorators;
813
+ let _refreshBackendCapabilities_decorators;
781
814
  let _getIntegration_decorators;
782
815
  let _createIntegration_decorators;
783
816
  let _updateIntegration_decorators;
@@ -808,6 +841,8 @@ let AwikiService = (() => {
808
841
  let _prepareRootTransfer_decorators;
809
842
  let _confirmRootTransfer_decorators;
810
843
  let _updateDisplayName_decorators;
844
+ let _setAvatar_decorators;
845
+ let _clearAvatar_decorators;
811
846
  let _getProfile_decorators;
812
847
  let _updateProfile_decorators;
813
848
  let _sendRecoveryOtp_decorators;
@@ -820,6 +855,7 @@ let AwikiService = (() => {
820
855
  let _resolvePeer_decorators;
821
856
  let _createGroup_decorators;
822
857
  let _getGroup_decorators;
858
+ let _repairGroupSecurity_decorators;
823
859
  let _joinGroup_decorators;
824
860
  let _leaveGroup_decorators;
825
861
  let _getDisplayProfiles_decorators;
@@ -847,6 +883,7 @@ let AwikiService = (() => {
847
883
  static {
848
884
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
849
885
  _getConfig_decorators = [Remote];
886
+ _refreshBackendCapabilities_decorators = [Remote];
850
887
  _getIntegration_decorators = [Remote];
851
888
  _createIntegration_decorators = [Remote];
852
889
  _updateIntegration_decorators = [Remote];
@@ -877,6 +914,8 @@ let AwikiService = (() => {
877
914
  _prepareRootTransfer_decorators = [Remote];
878
915
  _confirmRootTransfer_decorators = [Remote];
879
916
  _updateDisplayName_decorators = [Remote];
917
+ _setAvatar_decorators = [Remote];
918
+ _clearAvatar_decorators = [Remote];
880
919
  _getProfile_decorators = [Remote];
881
920
  _updateProfile_decorators = [Remote];
882
921
  _sendRecoveryOtp_decorators = [Remote];
@@ -889,6 +928,7 @@ let AwikiService = (() => {
889
928
  _resolvePeer_decorators = [Remote];
890
929
  _createGroup_decorators = [Remote];
891
930
  _getGroup_decorators = [Remote];
931
+ _repairGroupSecurity_decorators = [Remote];
892
932
  _joinGroup_decorators = [Remote];
893
933
  _leaveGroup_decorators = [Remote];
894
934
  _getDisplayProfiles_decorators = [Remote];
@@ -913,6 +953,7 @@ let AwikiService = (() => {
913
953
  _downloadMailAttachment_decorators = [Remote];
914
954
  _clearLocalData_decorators = [Remote];
915
955
  __esDecorate(this, null, _getConfig_decorators, { kind: "method", name: "getConfig", static: false, private: false, access: { has: obj => "getConfig" in obj, get: obj => obj.getConfig }, metadata: _metadata }, null, _instanceExtraInitializers);
956
+ __esDecorate(this, null, _refreshBackendCapabilities_decorators, { kind: "method", name: "refreshBackendCapabilities", static: false, private: false, access: { has: obj => "refreshBackendCapabilities" in obj, get: obj => obj.refreshBackendCapabilities }, metadata: _metadata }, null, _instanceExtraInitializers);
916
957
  __esDecorate(this, null, _getIntegration_decorators, { kind: "method", name: "getIntegration", static: false, private: false, access: { has: obj => "getIntegration" in obj, get: obj => obj.getIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
917
958
  __esDecorate(this, null, _createIntegration_decorators, { kind: "method", name: "createIntegration", static: false, private: false, access: { has: obj => "createIntegration" in obj, get: obj => obj.createIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
918
959
  __esDecorate(this, null, _updateIntegration_decorators, { kind: "method", name: "updateIntegration", static: false, private: false, access: { has: obj => "updateIntegration" in obj, get: obj => obj.updateIntegration }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -943,6 +984,8 @@ let AwikiService = (() => {
943
984
  __esDecorate(this, null, _prepareRootTransfer_decorators, { kind: "method", name: "prepareRootTransfer", static: false, private: false, access: { has: obj => "prepareRootTransfer" in obj, get: obj => obj.prepareRootTransfer }, metadata: _metadata }, null, _instanceExtraInitializers);
944
985
  __esDecorate(this, null, _confirmRootTransfer_decorators, { kind: "method", name: "confirmRootTransfer", static: false, private: false, access: { has: obj => "confirmRootTransfer" in obj, get: obj => obj.confirmRootTransfer }, metadata: _metadata }, null, _instanceExtraInitializers);
945
986
  __esDecorate(this, null, _updateDisplayName_decorators, { kind: "method", name: "updateDisplayName", static: false, private: false, access: { has: obj => "updateDisplayName" in obj, get: obj => obj.updateDisplayName }, metadata: _metadata }, null, _instanceExtraInitializers);
987
+ __esDecorate(this, null, _setAvatar_decorators, { kind: "method", name: "setAvatar", static: false, private: false, access: { has: obj => "setAvatar" in obj, get: obj => obj.setAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
988
+ __esDecorate(this, null, _clearAvatar_decorators, { kind: "method", name: "clearAvatar", static: false, private: false, access: { has: obj => "clearAvatar" in obj, get: obj => obj.clearAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
946
989
  __esDecorate(this, null, _getProfile_decorators, { kind: "method", name: "getProfile", static: false, private: false, access: { has: obj => "getProfile" in obj, get: obj => obj.getProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
947
990
  __esDecorate(this, null, _updateProfile_decorators, { kind: "method", name: "updateProfile", static: false, private: false, access: { has: obj => "updateProfile" in obj, get: obj => obj.updateProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
948
991
  __esDecorate(this, null, _sendRecoveryOtp_decorators, { kind: "method", name: "sendRecoveryOtp", static: false, private: false, access: { has: obj => "sendRecoveryOtp" in obj, get: obj => obj.sendRecoveryOtp }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -955,6 +998,7 @@ let AwikiService = (() => {
955
998
  __esDecorate(this, null, _resolvePeer_decorators, { kind: "method", name: "resolvePeer", static: false, private: false, access: { has: obj => "resolvePeer" in obj, get: obj => obj.resolvePeer }, metadata: _metadata }, null, _instanceExtraInitializers);
956
999
  __esDecorate(this, null, _createGroup_decorators, { kind: "method", name: "createGroup", static: false, private: false, access: { has: obj => "createGroup" in obj, get: obj => obj.createGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
957
1000
  __esDecorate(this, null, _getGroup_decorators, { kind: "method", name: "getGroup", static: false, private: false, access: { has: obj => "getGroup" in obj, get: obj => obj.getGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
1001
+ __esDecorate(this, null, _repairGroupSecurity_decorators, { kind: "method", name: "repairGroupSecurity", static: false, private: false, access: { has: obj => "repairGroupSecurity" in obj, get: obj => obj.repairGroupSecurity }, metadata: _metadata }, null, _instanceExtraInitializers);
958
1002
  __esDecorate(this, null, _joinGroup_decorators, { kind: "method", name: "joinGroup", static: false, private: false, access: { has: obj => "joinGroup" in obj, get: obj => obj.joinGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
959
1003
  __esDecorate(this, null, _leaveGroup_decorators, { kind: "method", name: "leaveGroup", static: false, private: false, access: { has: obj => "leaveGroup" in obj, get: obj => obj.leaveGroup }, metadata: _metadata }, null, _instanceExtraInitializers);
960
1004
  __esDecorate(this, null, _getDisplayProfiles_decorators, { kind: "method", name: "getDisplayProfiles", static: false, private: false, access: { has: obj => "getDisplayProfiles" in obj, get: obj => obj.getDisplayProfiles }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -997,6 +1041,15 @@ let AwikiService = (() => {
997
1041
  tenantSwitching = false;
998
1042
  tenantParticipants = new Set();
999
1043
  activeCapabilities;
1044
+ backendFeatures = {};
1045
+ backendDiagnostics = {};
1046
+ backendFeaturesTask;
1047
+ backendFeaturesRevision = -1;
1048
+ backendSubscriptions = new Map();
1049
+ backendSubscriptionPending = new Set();
1050
+ backendAttachmentLimit;
1051
+ capabilityDiscoveryTask;
1052
+ capabilityDiscoveryAt = 0;
1000
1053
  updatePolicyStatus;
1001
1054
  updatePolicyRequest;
1002
1055
  updatePolicyTask;
@@ -1097,6 +1150,7 @@ let AwikiService = (() => {
1097
1150
  registerAwikiTools(ctx, this);
1098
1151
  ctx.effect(() => async () => {
1099
1152
  this.updatePolicyRequest?.abort();
1153
+ this.stopBackendSubscriptions();
1100
1154
  const provider = this.provider;
1101
1155
  if (provider !== undefined)
1102
1156
  await this.disposeProvider(provider);
@@ -1196,12 +1250,38 @@ let AwikiService = (() => {
1196
1250
  this.conversationPreferenceStore = new AwikiConversationPreferenceStore(options.stateRoot);
1197
1251
  this.mailListClient = new AwikiMailListClient(options.mailServiceUrl, this.externalHttpAuth);
1198
1252
  this.integrationClient = undefined;
1253
+ this.stopBackendSubscriptions();
1254
+ this.backendFeatures = {};
1255
+ this.backendDiagnostics = {};
1256
+ this.backendFeaturesRevision = -1;
1257
+ this.backendFeaturesTask = undefined;
1258
+ this.backendAttachmentLimit = undefined;
1259
+ this.capabilityDiscoveryTask = undefined;
1260
+ this.capabilityDiscoveryAt = 0;
1199
1261
  this.activeCapabilities = {
1200
1262
  tenantId: tenant.tenantId,
1201
1263
  generation: this.runtimeGeneration,
1202
1264
  online: false,
1203
1265
  handleRecoveryPhoneEnabled: false,
1204
1266
  };
1267
+ const metadata = new TenantCapabilityCache(options.stateRoot, options.userServiceUrl).read(value => {
1268
+ const services = {
1269
+ model_proxy: { enabled: true, protocol_version: 1, base_url: value.modelProxyBaseUrl },
1270
+ guest_gateway: { enabled: true, protocol_version: 1, base_url: value.guestGatewayBaseUrl },
1271
+ };
1272
+ const modelProxyBaseUrl = publishedServiceBaseUrl(services, 'model_proxy', this.resolved.allowInsecureLoopbackForTesting);
1273
+ const guestGatewayBaseUrl = publishedServiceBaseUrl(services, 'guest_gateway', false);
1274
+ return {
1275
+ handleRecoveryPhoneEnabled: value.handleRecoveryPhoneEnabled,
1276
+ ...modelProxyBaseUrl === undefined ? {} : { modelProxyBaseUrl },
1277
+ ...guestGatewayBaseUrl === undefined ? {} : { guestGatewayBaseUrl },
1278
+ };
1279
+ });
1280
+ if (metadata !== undefined) {
1281
+ this.activeCapabilities = { ...this.activeCapabilities, ...metadata, online: true };
1282
+ this.integrationClient = metadata.guestGatewayBaseUrl === undefined ? undefined
1283
+ : new AwikiIntegrationClient(metadata.guestGatewayBaseUrl, this.externalHttpAuth);
1284
+ }
1205
1285
  this.signedOut = undefined;
1206
1286
  this.activeIdentityDid = undefined;
1207
1287
  this.pendingDeviceJoin = undefined;
@@ -1380,6 +1460,12 @@ let AwikiService = (() => {
1380
1460
  let committed = false;
1381
1461
  try {
1382
1462
  await this.sessionMutation;
1463
+ // Lock both scopes before disposal/open. A crash or rollback must not log in.
1464
+ await this.sessionStore.signOut();
1465
+ this.signedOut = true;
1466
+ this.sessionRevision += 1;
1467
+ this.activeIdentityDid = undefined;
1468
+ await new AwikiSessionStore(registry.stateRoot(target)).signOut();
1383
1469
  this.invalidateSummaries();
1384
1470
  for (const participant of this.tenantParticipants) {
1385
1471
  await participant.prepareSwitch(context);
@@ -1390,8 +1476,14 @@ let AwikiService = (() => {
1390
1476
  targetOpened = true;
1391
1477
  // The factory returns an adapter immediately; its native Core opens asynchronously.
1392
1478
  // A local identity read proves both open and Registry loading before committing the switch.
1393
- await targetProvider.client.getIdentity();
1394
- await Promise.all([this.discoverTenantCapabilities(3_000), this.refreshUpdatePolicy()]);
1479
+ const targetIdentity = await targetProvider.client.getIdentity();
1480
+ if (targetIdentity === null) {
1481
+ // An empty scope has no session to restore. Keep the normal registration
1482
+ // entry available; only scopes with retained credentials need the lock.
1483
+ await this.sessionStore.signIn();
1484
+ this.signedOut = false;
1485
+ }
1486
+ void Promise.all([this.refreshTenantCapabilities(), this.refreshUpdatePolicy()]).catch(() => undefined);
1395
1487
  registry.commitActive(target.tenantId);
1396
1488
  committed = true;
1397
1489
  for (const participant of prepared)
@@ -1491,17 +1583,22 @@ let AwikiService = (() => {
1491
1583
  */
1492
1584
  async getConfig() {
1493
1585
  this.ensureTenantOptions();
1494
- const capabilities = await this.discoverTenantCapabilities(10_000);
1495
- if (this.tenantSwitching
1496
- || capabilities.generation !== this.runtimeGeneration
1497
- || capabilities.tenantId !== this.activeTenant?.tenantId) {
1586
+ const requestedGeneration = this.runtimeGeneration;
1587
+ if (requestedGeneration !== this.runtimeGeneration)
1498
1588
  return { ok: false, error: failure('conflict') };
1499
- }
1589
+ // Config and local session/history reads never join discovery's network wait.
1590
+ const capabilities = this.activeCapabilities;
1591
+ this.refreshBackendProjection();
1592
+ void this.refreshTenantCapabilities(false).catch(() => undefined);
1593
+ const serverFeatures = this.backendFeatures;
1594
+ const attachmentMaxBytes = this.backendAttachmentLimit ?? this.resolved.attachmentMaxBytes;
1500
1595
  return {
1501
1596
  ok: true,
1502
1597
  value: {
1598
+ serverFeatures,
1599
+ serverFeatureDiagnostics: this.backendDiagnostics,
1503
1600
  pollIntervalMs: this.resolved.pollIntervalMs,
1504
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1601
+ attachmentMaxBytes,
1505
1602
  mailAttachmentMaxCount: this.resolved.mailAttachmentMaxCount,
1506
1603
  mailAttachmentMaxBytes: Math.min(this.resolved.mailAttachmentMaxBytes, MAIL_HTTP_UPLOAD_MAX_BYTES),
1507
1604
  mailAttachmentDownloadMaxBytes: this.resolved.mailAttachmentMaxBytes,
@@ -1511,6 +1608,7 @@ let AwikiService = (() => {
1511
1608
  ? {}
1512
1609
  : { integrationGuideUrl: new URL('/guest/guide/integration', capabilities.guestGatewayBaseUrl).toString() },
1513
1610
  tenantId: capabilities.tenantId,
1611
+ tenantStorageScopeId: this.activeTenant.storageScopeId,
1514
1612
  tenantGeneration: capabilities.generation,
1515
1613
  tenantOnline: capabilities.online,
1516
1614
  services: {
@@ -1520,6 +1618,15 @@ let AwikiService = (() => {
1520
1618
  },
1521
1619
  };
1522
1620
  }
1621
+ /** Explicit discovery retry; ordinary config reads retain their zero-argument contract. */
1622
+ async refreshBackendCapabilities() {
1623
+ this.ensureTenantOptions();
1624
+ const generation = this.runtimeGeneration;
1625
+ await Promise.allSettled([this.refreshTenantCapabilities(true), this.refreshBackendProjection(true)]);
1626
+ if (generation !== this.runtimeGeneration)
1627
+ return { ok: false, error: failure('conflict') };
1628
+ return this.getConfig();
1629
+ }
1523
1630
  /** Trusted same-process capability binding for optional tenant participants. */
1524
1631
  getTenantCapabilities() {
1525
1632
  const capabilities = this.activeCapabilities;
@@ -1528,9 +1635,124 @@ let AwikiService = (() => {
1528
1635
  return { ...capabilities };
1529
1636
  }
1530
1637
  /** Refresh the active tenant's optional service advertisement without affecting Core availability. */
1531
- refreshTenantCapabilities() {
1638
+ refreshTenantCapabilities(force = true) {
1532
1639
  this.ensureTenantOptions();
1533
- return this.discoverTenantCapabilities(10_000);
1640
+ if (this.capabilityDiscoveryTask !== undefined)
1641
+ return this.capabilityDiscoveryTask;
1642
+ if (!force && Date.now() - this.capabilityDiscoveryAt < 300_000 && this.activeCapabilities !== undefined)
1643
+ return Promise.resolve(this.activeCapabilities);
1644
+ const generation = this.runtimeGeneration;
1645
+ const task = this.discoverTenantCapabilities(10_000).finally(() => {
1646
+ if (this.runtimeGeneration === generation && this.capabilityDiscoveryTask === task) {
1647
+ this.capabilityDiscoveryTask = undefined;
1648
+ this.capabilityDiscoveryAt = Date.now();
1649
+ }
1650
+ });
1651
+ this.capabilityDiscoveryTask = task;
1652
+ return task;
1653
+ }
1654
+ refreshBackendProjection(force = false) {
1655
+ const provider = this.provider;
1656
+ if (provider === undefined)
1657
+ return Promise.resolve();
1658
+ if (this.backendFeaturesTask !== undefined)
1659
+ return this.backendFeaturesTask;
1660
+ const revision = this.sessionRevision;
1661
+ const generation = this.runtimeGeneration;
1662
+ if (this.backendFeaturesRevision !== revision) {
1663
+ // Only public facts survive a sign-out or identity change in this tenant.
1664
+ this.backendFeatures = Object.fromEntries(Object.entries(this.backendFeatures).filter(([key]) => key.includes('.')));
1665
+ this.backendAttachmentLimit = undefined;
1666
+ this.backendDiagnostics = this.backendDiagnostics.public === undefined ? {} : { public: this.backendDiagnostics.public };
1667
+ this.backendFeaturesRevision = revision;
1668
+ }
1669
+ const current = () => provider === this.provider && revision === this.sessionRevision && generation === this.runtimeGeneration;
1670
+ const task = (async () => {
1671
+ const authenticated = !await this.isSignedOut() && await provider.client.getIdentity() !== null;
1672
+ await Promise.all([false, ...(authenticated ? [true] : [])].map(async (local) => {
1673
+ try {
1674
+ this.observeBackendProjection(provider, local, current);
1675
+ const snapshot = await provider.client.backendCapabilities?.(local, force);
1676
+ if (!current())
1677
+ return;
1678
+ if (snapshot === undefined)
1679
+ return;
1680
+ // A subscribed source is authoritative: a query started before a
1681
+ // disable notification must never restore its older result.
1682
+ if (this.backendSubscriptions.has(local))
1683
+ return;
1684
+ this.recordBackendDiagnostics(local, snapshot);
1685
+ if (snapshot.status === 'unavailable')
1686
+ return;
1687
+ this.backendFeatures = { ...this.backendFeatures, ...projectBackendSnapshot(snapshot, local) };
1688
+ if (local)
1689
+ this.backendAttachmentLimit = backendAttachmentMaxBytes(snapshot, this.resolved.attachmentMaxBytes);
1690
+ if (this.backendFeatures['agents.managed_agents'] === 'unsupported')
1691
+ await this.detachAgentConsumer(provider);
1692
+ else if (this.backendFeatures['agents.managed_agents'] === 'supported')
1693
+ this.ensureAgentConsumer(provider);
1694
+ }
1695
+ catch (error) {
1696
+ // Preserve successful facts; auth failures keep their established category.
1697
+ if (current())
1698
+ this.ctx.logger('awiki-capabilities').debug('AWiki capability refresh failed: %s', error instanceof AwikiSdkError ? error.code : 'unavailable');
1699
+ }
1700
+ }));
1701
+ })().finally(() => { if (this.backendFeaturesTask === task)
1702
+ this.backendFeaturesTask = undefined; });
1703
+ this.backendFeaturesTask = task;
1704
+ void task.catch(() => undefined);
1705
+ return task;
1706
+ }
1707
+ recordBackendDiagnostics(local, snapshot) {
1708
+ this.backendDiagnostics = { ...this.backendDiagnostics, [local ? 'message' : 'public']: {
1709
+ ...typeof snapshot.fetched_at === 'number' ? { fetchedAt: snapshot.fetched_at } : {},
1710
+ ...snapshot.refresh_error === undefined ? {} : { refreshError: snapshot.refresh_error },
1711
+ } };
1712
+ }
1713
+ stopBackendSubscriptions() {
1714
+ for (const subscription of this.backendSubscriptions.values())
1715
+ subscription.stop();
1716
+ this.backendSubscriptions.clear();
1717
+ this.backendSubscriptionPending.clear();
1718
+ }
1719
+ observeBackendProjection(provider, local, current) {
1720
+ const subscribe = provider.client.subscribeBackendCapabilities;
1721
+ if (subscribe === undefined || this.backendSubscriptions.has(local) || this.backendSubscriptionPending.has(local))
1722
+ return;
1723
+ this.backendSubscriptionPending.add(local);
1724
+ void (async () => {
1725
+ const subscription = await subscribe.call(provider.client, local);
1726
+ if (!current()) {
1727
+ subscription.stop();
1728
+ return;
1729
+ }
1730
+ this.backendSubscriptionPending.delete(local);
1731
+ this.backendSubscriptions.set(local, subscription);
1732
+ try {
1733
+ while (current()) {
1734
+ const state = await subscription.nextUpdate();
1735
+ if (state === null || !current())
1736
+ break;
1737
+ this.recordBackendDiagnostics(local, state.snapshot);
1738
+ if (state.snapshot.status === 'unavailable')
1739
+ continue;
1740
+ this.backendFeatures = { ...this.backendFeatures, ...projectBackendSnapshot(state.snapshot, local) };
1741
+ if (local)
1742
+ this.backendAttachmentLimit = backendAttachmentMaxBytes(state.snapshot, this.resolved.attachmentMaxBytes);
1743
+ if (this.backendFeatures['agents.managed_agents'] === 'unsupported')
1744
+ await this.detachAgentConsumer(provider);
1745
+ else if (this.backendFeatures['agents.managed_agents'] === 'supported')
1746
+ this.ensureAgentConsumer(provider);
1747
+ }
1748
+ }
1749
+ finally {
1750
+ subscription.stop();
1751
+ if (this.backendSubscriptions.get(local) === subscription)
1752
+ this.backendSubscriptions.delete(local);
1753
+ }
1754
+ })().catch(() => { if (current())
1755
+ this.backendSubscriptionPending.delete(local); });
1534
1756
  }
1535
1757
  async discoverTenantCapabilities(timeoutMs) {
1536
1758
  const options = this.activeClientOptions;
@@ -1538,12 +1760,10 @@ let AwikiService = (() => {
1538
1760
  if (options === undefined || tenant === undefined)
1539
1761
  throw new ProviderUnavailableError();
1540
1762
  const generation = this.runtimeGeneration;
1541
- const fallback = {
1542
- tenantId: tenant.tenantId,
1543
- generation,
1544
- online: false,
1545
- handleRecoveryPhoneEnabled: false,
1546
- };
1763
+ const previous = this.activeCapabilities;
1764
+ const fallback = previous?.tenantId === tenant.tenantId && previous.generation === generation
1765
+ ? { ...previous }
1766
+ : { tenantId: tenant.tenantId, generation, online: false, handleRecoveryPhoneEnabled: false };
1547
1767
  const publish = (capabilities) => {
1548
1768
  if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
1549
1769
  this.activeCapabilities = capabilities;
@@ -1558,6 +1778,7 @@ let AwikiService = (() => {
1558
1778
  try {
1559
1779
  const endpoint = new URL('/user-service/v1/server-info', options.userServiceUrl);
1560
1780
  endpoint.searchParams.set('client_platform', 'dsh');
1781
+ endpoint.searchParams.set('capabilities_version', '1');
1561
1782
  const response = await fetch(endpoint, {
1562
1783
  method: 'GET',
1563
1784
  headers: { accept: 'application/json', 'cache-control': 'no-store' },
@@ -1571,13 +1792,19 @@ let AwikiService = (() => {
1571
1792
  const value = text === undefined ? undefined : JSON.parse(text);
1572
1793
  if (typeof value !== 'object' || value === null || value.schema_version !== 1)
1573
1794
  return publish(fallback);
1795
+ const versioned = value.capabilities_version === 1;
1796
+ if (!versioned) {
1797
+ if ('capabilities_version' in value)
1798
+ return publish({ tenantId: tenant.tenantId, generation, online: true, handleRecoveryPhoneEnabled: false });
1799
+ return publish(fallback);
1800
+ }
1574
1801
  const methods = value.identity?.handle_recovery?.methods;
1575
- const handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1802
+ const handleRecoveryPhoneEnabled = versioned && Array.isArray(methods) && methods.some((method) => (typeof method === 'object' && method !== null
1576
1803
  && method.id === 'phone'
1577
1804
  && method.enabled === true
1578
1805
  && method.verification?.required === true
1579
1806
  && method.verification?.type === 'sms_otp'));
1580
- const services = value.services;
1807
+ const services = versioned ? value.services : undefined;
1581
1808
  const modelProxyBaseUrl = publishedServiceBaseUrl(services, 'model_proxy', this.resolved.allowInsecureLoopbackForTesting);
1582
1809
  const guestGatewayBaseUrl = publishedServiceBaseUrl(services, 'guest_gateway', false);
1583
1810
  const capabilities = {
@@ -1588,6 +1815,15 @@ let AwikiService = (() => {
1588
1815
  ...modelProxyBaseUrl === undefined ? {} : { modelProxyBaseUrl },
1589
1816
  ...guestGatewayBaseUrl === undefined ? {} : { guestGatewayBaseUrl },
1590
1817
  };
1818
+ if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
1819
+ const saved = new TenantCapabilityCache(options.stateRoot, options.userServiceUrl).write({
1820
+ handleRecoveryPhoneEnabled,
1821
+ ...modelProxyBaseUrl === undefined ? {} : { modelProxyBaseUrl },
1822
+ ...guestGatewayBaseUrl === undefined ? {} : { guestGatewayBaseUrl },
1823
+ });
1824
+ if (!saved)
1825
+ this.ctx.logger('awiki-capabilities').debug('AWiki capability metadata persistence failed');
1826
+ }
1591
1827
  return publish(capabilities);
1592
1828
  }
1593
1829
  catch {
@@ -1670,11 +1906,6 @@ let AwikiService = (() => {
1670
1906
  return this.mutateSession(async () => {
1671
1907
  if (await this.isSignedOut())
1672
1908
  return { ok: true, value: { status: 'signed-out' } };
1673
- const identity = await this.run(client => client.getIdentity(), { allowSignedOut: true });
1674
- if (!identity.ok)
1675
- return identity;
1676
- if (identity.value === null)
1677
- return { ok: false, error: failure('not-registered') };
1678
1909
  try {
1679
1910
  await this.sessionStore.signOut();
1680
1911
  this.signedOut = true;
@@ -1683,8 +1914,10 @@ let AwikiService = (() => {
1683
1914
  const session = { status: 'signed-out' };
1684
1915
  this.publishSession(session);
1685
1916
  const provider = this.provider;
1686
- if (provider !== undefined)
1917
+ if (provider !== undefined) {
1687
1918
  await this.stopProviderRuntime(provider);
1919
+ await this.updateBackendDiscoveryLifecycle(provider, false);
1920
+ }
1688
1921
  return { ok: true, value: session };
1689
1922
  }
1690
1923
  catch {
@@ -1708,8 +1941,10 @@ let AwikiService = (() => {
1708
1941
  const session = { status: 'active', identity: identity.value };
1709
1942
  this.publishSession(session);
1710
1943
  const provider = this.provider;
1711
- if (provider !== undefined)
1944
+ if (provider !== undefined) {
1712
1945
  this.ensureProviderRuntime(provider);
1946
+ await this.updateBackendDiscoveryLifecycle(provider, true);
1947
+ }
1713
1948
  return { ok: true, value: session };
1714
1949
  }
1715
1950
  catch {
@@ -1896,6 +2131,9 @@ let AwikiService = (() => {
1896
2131
  const recoveries = await client.listPendingRecoveries();
1897
2132
  const pendingRegistrations = await client.pendingIdentityRegistrations();
1898
2133
  const creationMethods = await client.identityCreationMethods().catch(() => []);
2134
+ const backend = await client.backendCapabilities?.(false);
2135
+ const registrationHandleOnly = backend?.capabilities['registration.handle_only']?.support === 'supported'
2136
+ && (backend.status === 'confirmed' || backend.status === 'stale');
1899
2137
  const identity = await client.getIdentity();
1900
2138
  const capabilities = identity === null ? undefined : await client.identityMethodCapabilities(identity.did);
1901
2139
  const domain = this.activeClientOptions?.userServiceDomain;
@@ -1909,6 +2147,7 @@ let AwikiService = (() => {
1909
2147
  },
1910
2148
  joining,
1911
2149
  creationMethods,
2150
+ registrationHandleOnly,
1912
2151
  pendingRegistrations,
1913
2152
  ...capabilities === undefined ? {} : { methodCapabilities: capabilities },
1914
2153
  recoveries: recoveries.filter(value => domain !== undefined && value.fullHandle.slice(value.fullHandle.indexOf('.') + 1) === domain),
@@ -2164,6 +2403,18 @@ let AwikiService = (() => {
2164
2403
  this.activeIdentityDid = result.value.did;
2165
2404
  return result;
2166
2405
  }
2406
+ /** Replace only the current authenticated human account's avatar. */
2407
+ setAvatar(request) {
2408
+ if (!validAvatarRequest(request, true))
2409
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
2410
+ return this.run(client => client.setAvatar(request));
2411
+ }
2412
+ /** Restore the current account's default character avatar. */
2413
+ clearAvatar(request) {
2414
+ if (!validAvatarRequest(request, false))
2415
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
2416
+ return this.run(client => client.clearAvatar(request));
2417
+ }
2167
2418
  /** Return the public editable profile for the active identity. */
2168
2419
  getProfile() {
2169
2420
  return this.run(client => client.getProfile());
@@ -2255,7 +2506,14 @@ let AwikiService = (() => {
2255
2506
  const normalized = normalizeCreateGroupRequest(request);
2256
2507
  if (normalized === undefined)
2257
2508
  return { ok: false, error: failure('invalid-request') };
2258
- const created = await this.run(client => client.createGroup(normalized.name));
2509
+ const created = await this.run(async (client) => {
2510
+ await requireBackendFeature(client, 'groups');
2511
+ if (normalized.encrypted)
2512
+ await requireBackendFeature(client, 'group_e2ee');
2513
+ if (normalized.members.length > 0)
2514
+ await requireBackendFeature(client, 'group_admin_add');
2515
+ return client.createGroup(normalized.name, normalized.encrypted, normalized.operationId);
2516
+ });
2259
2517
  if (!created.ok)
2260
2518
  return created;
2261
2519
  const addedMembers = [];
@@ -2279,12 +2537,19 @@ let AwikiService = (() => {
2279
2537
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2280
2538
  return this.run(client => client.getGroup(groupDid));
2281
2539
  }
2540
+ /** Resume owner-held group encryption maintenance through Core. */
2541
+ repairGroupSecurity(request) {
2542
+ const groupDid = normalizeGroupDid(request?.groupDid);
2543
+ if (groupDid === undefined)
2544
+ return Promise.resolve({ ok: false, error: failure('invalid-request') });
2545
+ return this.run(async (client) => { await requireBackendFeature(client, 'group_e2ee'); return client.repairGroupSecurity(groupDid); });
2546
+ }
2282
2547
  /** Join one open group and return its authoritative membership state. */
2283
2548
  joinGroup(request) {
2284
2549
  const groupDid = normalizeGroupDid(request?.groupDid);
2285
2550
  if (groupDid === undefined)
2286
2551
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2287
- return this.run(client => client.joinGroup(groupDid));
2552
+ return this.run(async (client) => { await requireBackendFeature(client, 'group_open_join'); return client.joinGroup(groupDid); });
2288
2553
  }
2289
2554
  /** Leave one group. Core rejects owner leave and unsupported security profiles. */
2290
2555
  async leaveGroup(request) {
@@ -2295,11 +2560,11 @@ let AwikiService = (() => {
2295
2560
  return result.ok ? { ok: true, value: { completed: true } } : result;
2296
2561
  }
2297
2562
  /** Read one authoritative versioned member page. */
2298
- getDisplayProfiles(peers) {
2299
- if (!Array.isArray(peers) || peers.length > 100 || peers.some(peer => typeof peer !== 'string' || !peer.startsWith('did:'))) {
2563
+ getDisplayProfiles(peers, refresh) {
2564
+ if ((refresh !== undefined && typeof refresh !== 'boolean') || !Array.isArray(peers) || peers.length > 100 || peers.some(peer => typeof peer !== 'string' || !peer.startsWith('did:'))) {
2300
2565
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2301
2566
  }
2302
- return this.run(client => client.getDisplayProfiles(peers));
2567
+ return this.run(client => client.getDisplayProfiles(peers, refresh));
2303
2568
  }
2304
2569
  listGroupMembers(request) {
2305
2570
  const groupDid = normalizeGroupDid(request?.groupDid);
@@ -2478,6 +2743,8 @@ let AwikiService = (() => {
2478
2743
  * @returns The accepted attachment message or a closed failure.
2479
2744
  */
2480
2745
  async sendAttachment(request) {
2746
+ if (request?.security !== undefined && request.security !== 'plain' && request.security !== 'e2ee-required')
2747
+ return { ok: false, error: failure('invalid-request') };
2481
2748
  if (!isMessageTarget(request?.target))
2482
2749
  return { ok: false, error: failure('invalid-request') };
2483
2750
  const decoded = decodeAttachment(request.bytesBase64, this.resolved.attachmentMaxBytes);
@@ -2485,6 +2752,7 @@ let AwikiService = (() => {
2485
2752
  return decoded;
2486
2753
  return this.run(async (client) => {
2487
2754
  const message = await client.sendAttachment({
2755
+ ...request.security === undefined ? {} : { security: request.security },
2488
2756
  target: request.target,
2489
2757
  attachment: {
2490
2758
  fileName: request.fileName,
@@ -2823,12 +3091,14 @@ let AwikiService = (() => {
2823
3091
  return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
2824
3092
  }
2825
3093
  async requireDeviceManager(client) {
3094
+ await requireBackendFeature(client, 'identity.multi_device');
2826
3095
  const current = await client.getCurrentDeviceSummary();
2827
3096
  if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2828
3097
  throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2829
3098
  }
2830
3099
  }
2831
3100
  async requireRootTransferRecipient(client, deviceId) {
3101
+ await requireBackendFeature(client, 'identity.root_transfer');
2832
3102
  if (!client.trustedUserPresenceSupported) {
2833
3103
  throw Object.assign(new Error('trusted user presence unsupported'), { name: 'AwikiSdkError', code: 'forbidden' });
2834
3104
  }
@@ -2961,6 +3231,7 @@ let AwikiService = (() => {
2961
3231
  }
2962
3232
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
2963
3233
  invalidateSummaries() {
3234
+ this.stopBackendSubscriptions();
2964
3235
  this.sessionRevision += 1;
2965
3236
  this.requestRefs.clear();
2966
3237
  this.requestSessions.clear();
@@ -3042,7 +3313,10 @@ let AwikiService = (() => {
3042
3313
  catch {
3043
3314
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
3044
3315
  }
3045
- return this.run(client => operation(client, request), options);
3316
+ return this.run(async (client) => {
3317
+ await requireBackendFeature(client, 'services.mail');
3318
+ return operation(client, request);
3319
+ }, options);
3046
3320
  }
3047
3321
  /** Read and cache the private Host-owned session marker. */
3048
3322
  async isSignedOut() {
@@ -3103,6 +3377,16 @@ let AwikiService = (() => {
3103
3377
  this.sessionMutation = pending.then(() => undefined, () => undefined);
3104
3378
  return pending;
3105
3379
  }
3380
+ /** Discovery is an optional projection, not authority for a committed session transition. */
3381
+ async updateBackendDiscoveryLifecycle(provider, active) {
3382
+ try {
3383
+ await provider.client.setBackendDiscoveryActive?.(active);
3384
+ }
3385
+ catch {
3386
+ // Do not include bridge errors: provider diagnostics may contain private context.
3387
+ this.ctx.logger('awiki-backend').warn('AWiki backend discovery lifecycle unavailable');
3388
+ }
3389
+ }
3106
3390
  /** Start identity realtime and the optional Agent consumer without blocking identity success. */
3107
3391
  ensureProviderRuntime(provider) {
3108
3392
  if (this.tenantSwitching)
@@ -3116,6 +3400,7 @@ let AwikiService = (() => {
3116
3400
  this.replaceProviderRuntime(provider, identityDid);
3117
3401
  return;
3118
3402
  }
3403
+ this.refreshBackendProjection();
3119
3404
  this.ensureRealtimeSupervisor(provider);
3120
3405
  this.ensureAgentConsumer(provider);
3121
3406
  }
@@ -3222,6 +3507,9 @@ let AwikiService = (() => {
3222
3507
  const logger = this.ctx.logger('awiki-listener');
3223
3508
  const startup = (async () => {
3224
3509
  await this.detachAgentConsumer(provider);
3510
+ const features = await projectBackendFeatures(provider.client, false);
3511
+ if (features['agents.managed_agents'] !== 'supported')
3512
+ return;
3225
3513
  if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
3226
3514
  return;
3227
3515
  const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);