@awiki/dsh-plugin 0.3.18 → 0.3.20

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 (59) hide show
  1. package/README.zh.md +11 -0
  2. package/lib/client.js +644 -100
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +389 -24
  5. package/lib/{package-version.generated-D0MSDRZn.mjs → package-version.generated-RNVj-2iA.mjs} +22 -2
  6. package/lib/provider.js +1 -1
  7. package/lib/typert.host.js +204 -138
  8. package/lib/typert.remote-client.d.ts +2 -0
  9. package/lib/typert.remote-client.d.ts.map +1 -1
  10. package/lib/typert.remote-client.js +204 -138
  11. package/lib/types/backend-capabilities.d.ts +12 -0
  12. package/lib/types/backend-capabilities.d.ts.map +1 -0
  13. package/lib/types/backend-capabilities.js +48 -0
  14. package/lib/types/backend-capabilities.js.map +1 -0
  15. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  16. package/lib/types/client/AwikiIdentityAccess.js +11 -2
  17. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  18. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  19. package/lib/types/client/AwikiOverlay.js +10 -7
  20. package/lib/types/client/AwikiOverlay.js.map +1 -1
  21. package/lib/types/client/AwikiSettingsSection.d.ts +1 -0
  22. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  23. package/lib/types/client/AwikiSettingsSection.js +5 -4
  24. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  25. package/lib/types/client/controller.d.ts +7 -0
  26. package/lib/types/client/controller.d.ts.map +1 -1
  27. package/lib/types/client/controller.js +39 -3
  28. package/lib/types/client/controller.js.map +1 -1
  29. package/lib/types/client/index.d.ts.map +1 -1
  30. package/lib/types/client/index.js +1 -0
  31. package/lib/types/client/index.js.map +1 -1
  32. package/lib/types/client/settings-locales.d.ts +1 -1
  33. package/lib/types/client/settings-locales.d.ts.map +1 -1
  34. package/lib/types/client/settings-locales.js +26 -2
  35. package/lib/types/client/settings-locales.js.map +1 -1
  36. package/lib/types/index.d.ts +18 -1
  37. package/lib/types/index.d.ts.map +1 -1
  38. package/lib/types/index.js +248 -25
  39. package/lib/types/index.js.map +1 -1
  40. package/lib/types/package-version.generated.d.ts +2 -2
  41. package/lib/types/package-version.generated.js +2 -2
  42. package/lib/types/provider-api.d.ts +19 -0
  43. package/lib/types/provider-api.d.ts.map +1 -1
  44. package/lib/types/sdk-adapter.d.ts +4 -0
  45. package/lib/types/sdk-adapter.d.ts.map +1 -1
  46. package/lib/types/sdk-adapter.js +20 -0
  47. package/lib/types/sdk-adapter.js.map +1 -1
  48. package/lib/types/tenant-capability-cache.d.ts +15 -0
  49. package/lib/types/tenant-capability-cache.d.ts.map +1 -0
  50. package/lib/types/tenant-capability-cache.js +45 -0
  51. package/lib/types/tenant-capability-cache.js.map +1 -0
  52. package/lib/types/types.d.ts +8 -1
  53. package/lib/types/types.d.ts.map +1 -1
  54. package/lib/types/types.js.map +1 -1
  55. package/lib/types/update-policy.d.ts +2 -2
  56. package/lib/types/update-policy.d.ts.map +1 -1
  57. package/lib/types/update-policy.js +10 -1
  58. package/lib/types/update-policy.js.map +1 -1
  59. package/package.json +4 -4
@@ -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',
@@ -155,6 +159,8 @@ const RESUMABLE_JOIN_PHASES = new Set([
155
159
  'approval_prepared',
156
160
  ]);
157
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.',
158
164
  'client-version-unsupported': 'This AWiki plugin version is not supported by the active tenant. Update the plugin or switch tenants.',
159
165
  'not-registered': 'No AWiki identity is registered for this deployment.',
160
166
  'signed-out': 'This installation is signed out of AWiki.',
@@ -274,7 +280,7 @@ function publishedServiceBaseUrl(services, name, allowInsecureLoopbackForTesting
274
280
  if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
275
281
  return undefined;
276
282
  const value = candidate;
277
- if (value.enabled !== true || typeof value.base_url !== 'string')
283
+ if (value.enabled !== true || value.protocol_version !== 1 || typeof value.base_url !== 'string')
278
284
  return undefined;
279
285
  try {
280
286
  const baseUrl = serviceUrl(`server-info.services.${name}.base_url`, value.base_url, allowInsecureLoopbackForTesting);
@@ -804,6 +810,7 @@ let AwikiService = (() => {
804
810
  let _classSuper = TypertRemoteService;
805
811
  let _instanceExtraInitializers = [];
806
812
  let _getConfig_decorators;
813
+ let _refreshBackendCapabilities_decorators;
807
814
  let _getIntegration_decorators;
808
815
  let _createIntegration_decorators;
809
816
  let _updateIntegration_decorators;
@@ -876,6 +883,7 @@ let AwikiService = (() => {
876
883
  static {
877
884
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
878
885
  _getConfig_decorators = [Remote];
886
+ _refreshBackendCapabilities_decorators = [Remote];
879
887
  _getIntegration_decorators = [Remote];
880
888
  _createIntegration_decorators = [Remote];
881
889
  _updateIntegration_decorators = [Remote];
@@ -945,6 +953,7 @@ let AwikiService = (() => {
945
953
  _downloadMailAttachment_decorators = [Remote];
946
954
  _clearLocalData_decorators = [Remote];
947
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);
948
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);
949
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);
950
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);
@@ -1032,6 +1041,15 @@ let AwikiService = (() => {
1032
1041
  tenantSwitching = false;
1033
1042
  tenantParticipants = new Set();
1034
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;
1035
1053
  updatePolicyStatus;
1036
1054
  updatePolicyRequest;
1037
1055
  updatePolicyTask;
@@ -1132,6 +1150,7 @@ let AwikiService = (() => {
1132
1150
  registerAwikiTools(ctx, this);
1133
1151
  ctx.effect(() => async () => {
1134
1152
  this.updatePolicyRequest?.abort();
1153
+ this.stopBackendSubscriptions();
1135
1154
  const provider = this.provider;
1136
1155
  if (provider !== undefined)
1137
1156
  await this.disposeProvider(provider);
@@ -1231,12 +1250,38 @@ let AwikiService = (() => {
1231
1250
  this.conversationPreferenceStore = new AwikiConversationPreferenceStore(options.stateRoot);
1232
1251
  this.mailListClient = new AwikiMailListClient(options.mailServiceUrl, this.externalHttpAuth);
1233
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;
1234
1261
  this.activeCapabilities = {
1235
1262
  tenantId: tenant.tenantId,
1236
1263
  generation: this.runtimeGeneration,
1237
1264
  online: false,
1238
1265
  handleRecoveryPhoneEnabled: false,
1239
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
+ }
1240
1285
  this.signedOut = undefined;
1241
1286
  this.activeIdentityDid = undefined;
1242
1287
  this.pendingDeviceJoin = undefined;
@@ -1438,7 +1483,7 @@ let AwikiService = (() => {
1438
1483
  await this.sessionStore.signIn();
1439
1484
  this.signedOut = false;
1440
1485
  }
1441
- await Promise.all([this.discoverTenantCapabilities(3_000), this.refreshUpdatePolicy()]);
1486
+ void Promise.all([this.refreshTenantCapabilities(), this.refreshUpdatePolicy()]).catch(() => undefined);
1442
1487
  registry.commitActive(target.tenantId);
1443
1488
  committed = true;
1444
1489
  for (const participant of prepared)
@@ -1538,17 +1583,22 @@ let AwikiService = (() => {
1538
1583
  */
1539
1584
  async getConfig() {
1540
1585
  this.ensureTenantOptions();
1541
- const capabilities = await this.discoverTenantCapabilities(10_000);
1542
- if (this.tenantSwitching
1543
- || capabilities.generation !== this.runtimeGeneration
1544
- || capabilities.tenantId !== this.activeTenant?.tenantId) {
1586
+ const requestedGeneration = this.runtimeGeneration;
1587
+ if (requestedGeneration !== this.runtimeGeneration)
1545
1588
  return { ok: false, error: failure('conflict') };
1546
- }
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;
1547
1595
  return {
1548
1596
  ok: true,
1549
1597
  value: {
1598
+ serverFeatures,
1599
+ serverFeatureDiagnostics: this.backendDiagnostics,
1550
1600
  pollIntervalMs: this.resolved.pollIntervalMs,
1551
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
1601
+ attachmentMaxBytes,
1552
1602
  mailAttachmentMaxCount: this.resolved.mailAttachmentMaxCount,
1553
1603
  mailAttachmentMaxBytes: Math.min(this.resolved.mailAttachmentMaxBytes, MAIL_HTTP_UPLOAD_MAX_BYTES),
1554
1604
  mailAttachmentDownloadMaxBytes: this.resolved.mailAttachmentMaxBytes,
@@ -1558,7 +1608,7 @@ let AwikiService = (() => {
1558
1608
  ? {}
1559
1609
  : { integrationGuideUrl: new URL('/guest/guide/integration', capabilities.guestGatewayBaseUrl).toString() },
1560
1610
  tenantId: capabilities.tenantId,
1561
- tenantStorageScopeId: this.activeTenant?.storageScopeId,
1611
+ tenantStorageScopeId: this.activeTenant.storageScopeId,
1562
1612
  tenantGeneration: capabilities.generation,
1563
1613
  tenantOnline: capabilities.online,
1564
1614
  services: {
@@ -1568,6 +1618,15 @@ let AwikiService = (() => {
1568
1618
  },
1569
1619
  };
1570
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
+ }
1571
1630
  /** Trusted same-process capability binding for optional tenant participants. */
1572
1631
  getTenantCapabilities() {
1573
1632
  const capabilities = this.activeCapabilities;
@@ -1576,9 +1635,124 @@ let AwikiService = (() => {
1576
1635
  return { ...capabilities };
1577
1636
  }
1578
1637
  /** Refresh the active tenant's optional service advertisement without affecting Core availability. */
1579
- refreshTenantCapabilities() {
1638
+ refreshTenantCapabilities(force = true) {
1580
1639
  this.ensureTenantOptions();
1581
- 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); });
1582
1756
  }
1583
1757
  async discoverTenantCapabilities(timeoutMs) {
1584
1758
  const options = this.activeClientOptions;
@@ -1586,12 +1760,10 @@ let AwikiService = (() => {
1586
1760
  if (options === undefined || tenant === undefined)
1587
1761
  throw new ProviderUnavailableError();
1588
1762
  const generation = this.runtimeGeneration;
1589
- const fallback = {
1590
- tenantId: tenant.tenantId,
1591
- generation,
1592
- online: false,
1593
- handleRecoveryPhoneEnabled: false,
1594
- };
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 };
1595
1767
  const publish = (capabilities) => {
1596
1768
  if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
1597
1769
  this.activeCapabilities = capabilities;
@@ -1606,6 +1778,7 @@ let AwikiService = (() => {
1606
1778
  try {
1607
1779
  const endpoint = new URL('/user-service/v1/server-info', options.userServiceUrl);
1608
1780
  endpoint.searchParams.set('client_platform', 'dsh');
1781
+ endpoint.searchParams.set('capabilities_version', '1');
1609
1782
  const response = await fetch(endpoint, {
1610
1783
  method: 'GET',
1611
1784
  headers: { accept: 'application/json', 'cache-control': 'no-store' },
@@ -1619,13 +1792,19 @@ let AwikiService = (() => {
1619
1792
  const value = text === undefined ? undefined : JSON.parse(text);
1620
1793
  if (typeof value !== 'object' || value === null || value.schema_version !== 1)
1621
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
+ }
1622
1801
  const methods = value.identity?.handle_recovery?.methods;
1623
- 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
1624
1803
  && method.id === 'phone'
1625
1804
  && method.enabled === true
1626
1805
  && method.verification?.required === true
1627
1806
  && method.verification?.type === 'sms_otp'));
1628
- const services = value.services;
1807
+ const services = versioned ? value.services : undefined;
1629
1808
  const modelProxyBaseUrl = publishedServiceBaseUrl(services, 'model_proxy', this.resolved.allowInsecureLoopbackForTesting);
1630
1809
  const guestGatewayBaseUrl = publishedServiceBaseUrl(services, 'guest_gateway', false);
1631
1810
  const capabilities = {
@@ -1636,6 +1815,15 @@ let AwikiService = (() => {
1636
1815
  ...modelProxyBaseUrl === undefined ? {} : { modelProxyBaseUrl },
1637
1816
  ...guestGatewayBaseUrl === undefined ? {} : { guestGatewayBaseUrl },
1638
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
+ }
1639
1827
  return publish(capabilities);
1640
1828
  }
1641
1829
  catch {
@@ -1726,8 +1914,10 @@ let AwikiService = (() => {
1726
1914
  const session = { status: 'signed-out' };
1727
1915
  this.publishSession(session);
1728
1916
  const provider = this.provider;
1729
- if (provider !== undefined)
1917
+ if (provider !== undefined) {
1730
1918
  await this.stopProviderRuntime(provider);
1919
+ await this.updateBackendDiscoveryLifecycle(provider, false);
1920
+ }
1731
1921
  return { ok: true, value: session };
1732
1922
  }
1733
1923
  catch {
@@ -1751,8 +1941,10 @@ let AwikiService = (() => {
1751
1941
  const session = { status: 'active', identity: identity.value };
1752
1942
  this.publishSession(session);
1753
1943
  const provider = this.provider;
1754
- if (provider !== undefined)
1944
+ if (provider !== undefined) {
1755
1945
  this.ensureProviderRuntime(provider);
1946
+ await this.updateBackendDiscoveryLifecycle(provider, true);
1947
+ }
1756
1948
  return { ok: true, value: session };
1757
1949
  }
1758
1950
  catch {
@@ -1939,6 +2131,9 @@ let AwikiService = (() => {
1939
2131
  const recoveries = await client.listPendingRecoveries();
1940
2132
  const pendingRegistrations = await client.pendingIdentityRegistrations();
1941
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');
1942
2137
  const identity = await client.getIdentity();
1943
2138
  const capabilities = identity === null ? undefined : await client.identityMethodCapabilities(identity.did);
1944
2139
  const domain = this.activeClientOptions?.userServiceDomain;
@@ -1952,6 +2147,7 @@ let AwikiService = (() => {
1952
2147
  },
1953
2148
  joining,
1954
2149
  creationMethods,
2150
+ registrationHandleOnly,
1955
2151
  pendingRegistrations,
1956
2152
  ...capabilities === undefined ? {} : { methodCapabilities: capabilities },
1957
2153
  recoveries: recoveries.filter(value => domain !== undefined && value.fullHandle.slice(value.fullHandle.indexOf('.') + 1) === domain),
@@ -2310,7 +2506,14 @@ let AwikiService = (() => {
2310
2506
  const normalized = normalizeCreateGroupRequest(request);
2311
2507
  if (normalized === undefined)
2312
2508
  return { ok: false, error: failure('invalid-request') };
2313
- const created = await this.run(client => client.createGroup(normalized.name, normalized.encrypted, normalized.operationId));
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
+ });
2314
2517
  if (!created.ok)
2315
2518
  return created;
2316
2519
  const addedMembers = [];
@@ -2339,14 +2542,14 @@ let AwikiService = (() => {
2339
2542
  const groupDid = normalizeGroupDid(request?.groupDid);
2340
2543
  if (groupDid === undefined)
2341
2544
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2342
- return this.run(client => client.repairGroupSecurity(groupDid));
2545
+ return this.run(async (client) => { await requireBackendFeature(client, 'group_e2ee'); return client.repairGroupSecurity(groupDid); });
2343
2546
  }
2344
2547
  /** Join one open group and return its authoritative membership state. */
2345
2548
  joinGroup(request) {
2346
2549
  const groupDid = normalizeGroupDid(request?.groupDid);
2347
2550
  if (groupDid === undefined)
2348
2551
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
2349
- return this.run(client => client.joinGroup(groupDid));
2552
+ return this.run(async (client) => { await requireBackendFeature(client, 'group_open_join'); return client.joinGroup(groupDid); });
2350
2553
  }
2351
2554
  /** Leave one group. Core rejects owner leave and unsupported security profiles. */
2352
2555
  async leaveGroup(request) {
@@ -2888,12 +3091,14 @@ let AwikiService = (() => {
2888
3091
  return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
2889
3092
  }
2890
3093
  async requireDeviceManager(client) {
3094
+ await requireBackendFeature(client, 'identity.multi_device');
2891
3095
  const current = await client.getCurrentDeviceSummary();
2892
3096
  if (!current.canManage || current.role !== 'admin' || current.readiness !== 'admin_ready') {
2893
3097
  throw Object.assign(new Error('device manager required'), { name: 'AwikiSdkError', code: 'forbidden' });
2894
3098
  }
2895
3099
  }
2896
3100
  async requireRootTransferRecipient(client, deviceId) {
3101
+ await requireBackendFeature(client, 'identity.root_transfer');
2897
3102
  if (!client.trustedUserPresenceSupported) {
2898
3103
  throw Object.assign(new Error('trusted user presence unsupported'), { name: 'AwikiSdkError', code: 'forbidden' });
2899
3104
  }
@@ -3026,6 +3231,7 @@ let AwikiService = (() => {
3026
3231
  }
3027
3232
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
3028
3233
  invalidateSummaries() {
3234
+ this.stopBackendSubscriptions();
3029
3235
  this.sessionRevision += 1;
3030
3236
  this.requestRefs.clear();
3031
3237
  this.requestSessions.clear();
@@ -3107,7 +3313,10 @@ let AwikiService = (() => {
3107
3313
  catch {
3108
3314
  return Promise.resolve({ ok: false, error: failure('invalid-request') });
3109
3315
  }
3110
- 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);
3111
3320
  }
3112
3321
  /** Read and cache the private Host-owned session marker. */
3113
3322
  async isSignedOut() {
@@ -3168,6 +3377,16 @@ let AwikiService = (() => {
3168
3377
  this.sessionMutation = pending.then(() => undefined, () => undefined);
3169
3378
  return pending;
3170
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
+ }
3171
3390
  /** Start identity realtime and the optional Agent consumer without blocking identity success. */
3172
3391
  ensureProviderRuntime(provider) {
3173
3392
  if (this.tenantSwitching)
@@ -3181,6 +3400,7 @@ let AwikiService = (() => {
3181
3400
  this.replaceProviderRuntime(provider, identityDid);
3182
3401
  return;
3183
3402
  }
3403
+ this.refreshBackendProjection();
3184
3404
  this.ensureRealtimeSupervisor(provider);
3185
3405
  this.ensureAgentConsumer(provider);
3186
3406
  }
@@ -3287,6 +3507,9 @@ let AwikiService = (() => {
3287
3507
  const logger = this.ctx.logger('awiki-listener');
3288
3508
  const startup = (async () => {
3289
3509
  await this.detachAgentConsumer(provider);
3510
+ const features = await projectBackendFeatures(provider.client, false);
3511
+ if (features['agents.managed_agents'] !== 'supported')
3512
+ return;
3290
3513
  if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation))
3291
3514
  return;
3292
3515
  const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);