@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
package/lib/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import { a as downloadedAttachment, c as successfulMailRecoveryObservability, l as isMessageTarget, n as DSH_AWIKI_PACKAGE_VERSION, o as failedMailRecoveryObservability, r as AwikiSdkError, s as fencedMailRecoveryObservability, t as DSH_AWIKI_MODEL_PROXY_PACKAGE_VERSION } from "./package-version.generated-D0MSDRZn.mjs";
2
- import "@deepseek-ai/cordis";
1
+ import { a as downloadedAttachment, c as successfulMailRecoveryObservability, l as isMessageTarget, n as DSH_AWIKI_PACKAGE_VERSION, o as failedMailRecoveryObservability, r as AwikiSdkError, s as fencedMailRecoveryObservability, t as DSH_AWIKI_MODEL_PROXY_PACKAGE_VERSION } from "./package-version.generated-RNVj-2iA.mjs";
3
2
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
4
- import { homedir } from "node:os";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import "@deepseek-ai/cordis";
6
+ import { homedir } from "node:os";
6
7
  import z from "@deepseek-ai/schemastery";
7
8
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
8
9
  import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
@@ -12,8 +13,122 @@ import { SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
12
13
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
13
14
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
14
15
  import { fileURLToPath } from "node:url";
15
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
16
16
  import { satisfies, validRange } from "semver";
17
+ //#region lib/types/tenant-capability-cache.js
18
+ /** Host-only metadata. Core owns feature truth; this contains no auth material. */
19
+ var TenantCapabilityCache = class {
20
+ key;
21
+ directory;
22
+ path;
23
+ constructor(stateRoot, endpoint) {
24
+ this.key = createHash("sha256").update(JSON.stringify([
25
+ endpoint,
26
+ stateRoot,
27
+ 1
28
+ ])).digest("hex");
29
+ this.directory = join(stateRoot, "cache", "tenant-capabilities-v1");
30
+ this.path = join(this.directory, `${this.key}.json`);
31
+ }
32
+ read(validate) {
33
+ try {
34
+ const bytes = readFileSync(this.path);
35
+ if (bytes.length > 16384) return void 0;
36
+ const value = JSON.parse(bytes.toString("utf8"));
37
+ if (value.version !== 1 || value.key !== this.key || typeof value.metadata?.handleRecoveryPhoneEnabled !== "boolean") return void 0;
38
+ return validate(value.metadata);
39
+ } catch {
40
+ return;
41
+ }
42
+ }
43
+ write(metadata) {
44
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
45
+ try {
46
+ mkdirSync(this.directory, {
47
+ recursive: true,
48
+ mode: 448
49
+ });
50
+ writeFileSync(temporary, JSON.stringify({
51
+ version: 1,
52
+ key: this.key,
53
+ metadata
54
+ }), {
55
+ mode: 384,
56
+ flag: "wx"
57
+ });
58
+ renameSync(temporary, this.path);
59
+ return true;
60
+ } catch {
61
+ try {
62
+ unlinkSync(temporary);
63
+ } catch {}
64
+ return false;
65
+ }
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region lib/types/backend-capabilities.js
70
+ /** Keep the client ceiling and only project a confirmed positive byte limit. */
71
+ function backendAttachmentMaxBytes(snapshot, clientLimit) {
72
+ if (snapshot?.status !== "confirmed" && snapshot?.status !== "stale") return clientLimit;
73
+ const capability = snapshot.capabilities.attachments;
74
+ if (capability?.support !== "supported") return clientLimit;
75
+ const raw = capability.limits?.max_object_bytes;
76
+ const value = typeof raw === "number" ? raw : typeof raw === "string" && /^\d+$/.test(raw) ? Number(raw) : NaN;
77
+ return Number.isSafeInteger(value) && value > 0 ? Math.min(clientLimit, value) : clientLimit;
78
+ }
79
+ const PUBLIC = [
80
+ "registration.handle_only",
81
+ "identity.multi_device",
82
+ "identity.device_revoke",
83
+ "identity.capability_upgrade",
84
+ "identity.root_transfer",
85
+ "agents.managed_agents",
86
+ "agents.daemon",
87
+ "services.mail",
88
+ "services.model_proxy",
89
+ "services.guest_gateway",
90
+ "account.state_sync",
91
+ "content.pages",
92
+ "content.sites",
93
+ "content.hosting",
94
+ "client_updates.policy"
95
+ ];
96
+ const MESSAGE = [
97
+ "direct",
98
+ "groups",
99
+ "attachments",
100
+ "direct_e2ee",
101
+ "group_e2ee",
102
+ "encrypted_attachments",
103
+ "group_open_join",
104
+ "group_admin_add"
105
+ ];
106
+ /** Project one source independently so a failed source cannot erase its peer. */
107
+ function projectBackendSnapshot(snapshot, authenticated) {
108
+ const result = {};
109
+ for (const key of authenticated ? MESSAGE : PUBLIC) {
110
+ const support = snapshot?.capabilities[key]?.support;
111
+ result[key] = (snapshot?.status === "confirmed" || snapshot?.status === "stale") && (support === "supported" || support === "unsupported") ? support : "unknown";
112
+ }
113
+ return result;
114
+ }
115
+ /** Copy only allowlisted support states. Browser never sees endpoints or SDK errors. */
116
+ async function projectBackendFeatures(client, authenticated) {
117
+ const sources = await Promise.allSettled([client.backendCapabilities?.(false, false), ...authenticated ? [client.backendCapabilities?.(true, false)] : []]);
118
+ const result = {};
119
+ for (const [index, source] of sources.entries()) if (source.status === "fulfilled") Object.assign(result, projectBackendSnapshot(source.value, index === 1));
120
+ else throw source.reason;
121
+ return result;
122
+ }
123
+ async function requireBackendFeature(client, key) {
124
+ const snapshot = await client.backendCapabilities?.(!key.includes("."), false);
125
+ const support = snapshot?.status === "confirmed" || snapshot?.status === "stale" ? snapshot.capabilities[key]?.support : "unknown";
126
+ if (support !== "supported") throw Object.assign(/* @__PURE__ */ new Error("Server feature unavailable"), {
127
+ name: "AwikiSdkError",
128
+ code: support === "unsupported" ? "backend-not-supported" : "backend-unconfirmed"
129
+ });
130
+ }
131
+ //#endregion
17
132
  //#region lib/types/builtin-tenant-config.js
18
133
  /** Build-injected two-slot tenant catalog with a source-tree default for tests. */
19
134
  function record$2(value) {
@@ -3718,6 +3833,7 @@ async function checkAwikiUpdatePolicy(options) {
3718
3833
  signal.throwIfAborted();
3719
3834
  const endpoint = new URL("/user-service/v1/server-info", origin);
3720
3835
  endpoint.searchParams.set("client_platform", "dsh");
3836
+ endpoint.searchParams.set("capabilities_version", "1");
3721
3837
  const response = await bounded((options.fetcher ?? fetch)(endpoint, {
3722
3838
  method: "GET",
3723
3839
  headers: {
@@ -3746,7 +3862,19 @@ async function checkAwikiUpdatePolicy(options) {
3746
3862
  const bytes = await bounded(readPolicyBody(response));
3747
3863
  signal.throwIfAborted();
3748
3864
  if (bytes.byteLength > MAX_POLICY_BYTES) throw new Error("policy response exceeds 1 MiB");
3749
- const policy = decodeServerInfoPolicy(JSON.parse(Buffer.from(bytes).toString("utf8")), origin, cached?.policy.policy_revision);
3865
+ const document = JSON.parse(Buffer.from(bytes).toString("utf8"));
3866
+ if (isRecord(document) && document.schema_version === 1 && document.capabilities_version === 1 && isRecord(document.client_updates) && isRecord(document.client_updates.policy) && document.client_updates.policy.protocol_version === 1 && document.client_updates.policy.enabled === false) return {
3867
+ ...cached === void 0 ? {
3868
+ ...base,
3869
+ usedCache: false,
3870
+ restricted: false,
3871
+ modelProxyRestricted: false
3872
+ } : statusFromPolicy(base, cached.policy, cached.checkedAt, false, true, options.installedRuntime),
3873
+ offline: false,
3874
+ policyUnavailable: true,
3875
+ checkState: "unavailable"
3876
+ };
3877
+ const policy = decodeServerInfoPolicy(document, origin, cached?.policy.policy_revision);
3750
3878
  if (policy === void 0) {
3751
3879
  rmSync(cachePath, { force: true });
3752
3880
  return {
@@ -4076,6 +4204,8 @@ const Config = z.object({
4076
4204
  summaryMaxInputBytes: z.number().default(DEFAULT_SUMMARY_MAX_INPUT_BYTES)
4077
4205
  });
4078
4206
  const FAILURE_CODES = /* @__PURE__ */ new Set([
4207
+ "backend-not-supported",
4208
+ "backend-unconfirmed",
4079
4209
  "client-version-unsupported",
4080
4210
  "not-registered",
4081
4211
  "signed-out",
@@ -4116,6 +4246,8 @@ const RESUMABLE_JOIN_PHASES = /* @__PURE__ */ new Set([
4116
4246
  "approval_prepared"
4117
4247
  ]);
4118
4248
  const FAILURE_MESSAGES = {
4249
+ "backend-not-supported": "This server does not support this feature.",
4250
+ "backend-unconfirmed": "Server features are not confirmed yet. Please retry.",
4119
4251
  "client-version-unsupported": "This AWiki plugin version is not supported by the active tenant. Update the plugin or switch tenants.",
4120
4252
  "not-registered": "No AWiki identity is registered for this deployment.",
4121
4253
  "signed-out": "This installation is signed out of AWiki.",
@@ -4198,7 +4330,7 @@ function publishedServiceBaseUrl(services, name, allowInsecureLoopbackForTesting
4198
4330
  const candidate = services[name];
4199
4331
  if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
4200
4332
  const value = candidate;
4201
- if (value.enabled !== true || typeof value.base_url !== "string") return void 0;
4333
+ if (value.enabled !== true || value.protocol_version !== 1 || typeof value.base_url !== "string") return void 0;
4202
4334
  try {
4203
4335
  const baseUrl = serviceUrl(`server-info.services.${name}.base_url`, value.base_url, allowInsecureLoopbackForTesting);
4204
4336
  if (new URL(baseUrl).search !== "") return void 0;
@@ -4649,6 +4781,7 @@ let AwikiService = (() => {
4649
4781
  let _classSuper = TypertRemoteService;
4650
4782
  let _instanceExtraInitializers = [];
4651
4783
  let _getConfig_decorators;
4784
+ let _refreshBackendCapabilities_decorators;
4652
4785
  let _getIntegration_decorators;
4653
4786
  let _createIntegration_decorators;
4654
4787
  let _updateIntegration_decorators;
@@ -4721,6 +4854,7 @@ let AwikiService = (() => {
4721
4854
  static {
4722
4855
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
4723
4856
  _getConfig_decorators = [Remote];
4857
+ _refreshBackendCapabilities_decorators = [Remote];
4724
4858
  _getIntegration_decorators = [Remote];
4725
4859
  _createIntegration_decorators = [Remote];
4726
4860
  _updateIntegration_decorators = [Remote];
@@ -4800,6 +4934,17 @@ let AwikiService = (() => {
4800
4934
  },
4801
4935
  metadata: _metadata
4802
4936
  }, null, _instanceExtraInitializers);
4937
+ __esDecorate(this, null, _refreshBackendCapabilities_decorators, {
4938
+ kind: "method",
4939
+ name: "refreshBackendCapabilities",
4940
+ static: false,
4941
+ private: false,
4942
+ access: {
4943
+ has: (obj) => "refreshBackendCapabilities" in obj,
4944
+ get: (obj) => obj.refreshBackendCapabilities
4945
+ },
4946
+ metadata: _metadata
4947
+ }, null, _instanceExtraInitializers);
4803
4948
  __esDecorate(this, null, _getIntegration_decorators, {
4804
4949
  kind: "method",
4805
4950
  name: "getIntegration",
@@ -5572,6 +5717,15 @@ let AwikiService = (() => {
5572
5717
  tenantSwitching = false;
5573
5718
  tenantParticipants = /* @__PURE__ */ new Set();
5574
5719
  activeCapabilities;
5720
+ backendFeatures = {};
5721
+ backendDiagnostics = {};
5722
+ backendFeaturesTask;
5723
+ backendFeaturesRevision = -1;
5724
+ backendSubscriptions = /* @__PURE__ */ new Map();
5725
+ backendSubscriptionPending = /* @__PURE__ */ new Set();
5726
+ backendAttachmentLimit;
5727
+ capabilityDiscoveryTask;
5728
+ capabilityDiscoveryAt = 0;
5575
5729
  updatePolicyStatus;
5576
5730
  updatePolicyRequest;
5577
5731
  updatePolicyTask;
@@ -5668,6 +5822,7 @@ let AwikiService = (() => {
5668
5822
  registerAwikiTools(ctx, this);
5669
5823
  ctx.effect(() => async () => {
5670
5824
  this.updatePolicyRequest?.abort();
5825
+ this.stopBackendSubscriptions();
5671
5826
  const provider = this.provider;
5672
5827
  if (provider !== void 0) await this.disposeProvider(provider);
5673
5828
  }, "awiki: dispose current client provider");
@@ -5751,12 +5906,49 @@ let AwikiService = (() => {
5751
5906
  this.conversationPreferenceStore = new AwikiConversationPreferenceStore(options.stateRoot);
5752
5907
  this.mailListClient = new AwikiMailListClient(options.mailServiceUrl, this.externalHttpAuth);
5753
5908
  this.integrationClient = void 0;
5909
+ this.stopBackendSubscriptions();
5910
+ this.backendFeatures = {};
5911
+ this.backendDiagnostics = {};
5912
+ this.backendFeaturesRevision = -1;
5913
+ this.backendFeaturesTask = void 0;
5914
+ this.backendAttachmentLimit = void 0;
5915
+ this.capabilityDiscoveryTask = void 0;
5916
+ this.capabilityDiscoveryAt = 0;
5754
5917
  this.activeCapabilities = {
5755
5918
  tenantId: tenant.tenantId,
5756
5919
  generation: this.runtimeGeneration,
5757
5920
  online: false,
5758
5921
  handleRecoveryPhoneEnabled: false
5759
5922
  };
5923
+ const metadata = new TenantCapabilityCache(options.stateRoot, options.userServiceUrl).read((value) => {
5924
+ const services = {
5925
+ model_proxy: {
5926
+ enabled: true,
5927
+ protocol_version: 1,
5928
+ base_url: value.modelProxyBaseUrl
5929
+ },
5930
+ guest_gateway: {
5931
+ enabled: true,
5932
+ protocol_version: 1,
5933
+ base_url: value.guestGatewayBaseUrl
5934
+ }
5935
+ };
5936
+ const modelProxyBaseUrl = publishedServiceBaseUrl(services, "model_proxy", this.resolved.allowInsecureLoopbackForTesting);
5937
+ const guestGatewayBaseUrl = publishedServiceBaseUrl(services, "guest_gateway", false);
5938
+ return {
5939
+ handleRecoveryPhoneEnabled: value.handleRecoveryPhoneEnabled,
5940
+ ...modelProxyBaseUrl === void 0 ? {} : { modelProxyBaseUrl },
5941
+ ...guestGatewayBaseUrl === void 0 ? {} : { guestGatewayBaseUrl }
5942
+ };
5943
+ });
5944
+ if (metadata !== void 0) {
5945
+ this.activeCapabilities = {
5946
+ ...this.activeCapabilities,
5947
+ ...metadata,
5948
+ online: true
5949
+ };
5950
+ this.integrationClient = metadata.guestGatewayBaseUrl === void 0 ? void 0 : new AwikiIntegrationClient(metadata.guestGatewayBaseUrl, this.externalHttpAuth);
5951
+ }
5760
5952
  this.signedOut = void 0;
5761
5953
  this.activeIdentityDid = void 0;
5762
5954
  this.pendingDeviceJoin = void 0;
@@ -5934,7 +6126,7 @@ let AwikiService = (() => {
5934
6126
  await this.sessionStore.signIn();
5935
6127
  this.signedOut = false;
5936
6128
  }
5937
- await Promise.all([this.discoverTenantCapabilities(3e3), this.refreshUpdatePolicy()]);
6129
+ Promise.all([this.refreshTenantCapabilities(), this.refreshUpdatePolicy()]).catch(() => void 0);
5938
6130
  registry.commitActive(target.tenantId);
5939
6131
  committed = true;
5940
6132
  for (const participant of prepared) await participant.commitSwitch?.(context);
@@ -6012,16 +6204,22 @@ let AwikiService = (() => {
6012
6204
  */
6013
6205
  async getConfig() {
6014
6206
  this.ensureTenantOptions();
6015
- const capabilities = await this.discoverTenantCapabilities(1e4);
6016
- if (this.tenantSwitching || capabilities.generation !== this.runtimeGeneration || capabilities.tenantId !== this.activeTenant?.tenantId) return {
6207
+ if (this.runtimeGeneration !== this.runtimeGeneration) return {
6017
6208
  ok: false,
6018
6209
  error: failure("conflict")
6019
6210
  };
6211
+ const capabilities = this.activeCapabilities;
6212
+ this.refreshBackendProjection();
6213
+ this.refreshTenantCapabilities(false).catch(() => void 0);
6214
+ const serverFeatures = this.backendFeatures;
6215
+ const attachmentMaxBytes = this.backendAttachmentLimit ?? this.resolved.attachmentMaxBytes;
6020
6216
  return {
6021
6217
  ok: true,
6022
6218
  value: {
6219
+ serverFeatures,
6220
+ serverFeatureDiagnostics: this.backendDiagnostics,
6023
6221
  pollIntervalMs: this.resolved.pollIntervalMs,
6024
- attachmentMaxBytes: this.resolved.attachmentMaxBytes,
6222
+ attachmentMaxBytes,
6025
6223
  mailAttachmentMaxCount: this.resolved.mailAttachmentMaxCount,
6026
6224
  mailAttachmentMaxBytes: Math.min(this.resolved.mailAttachmentMaxBytes, MAIL_HTTP_UPLOAD_MAX_BYTES),
6027
6225
  mailAttachmentDownloadMaxBytes: this.resolved.mailAttachmentMaxBytes,
@@ -6029,7 +6227,7 @@ let AwikiService = (() => {
6029
6227
  handleRecoveryPhoneEnabled: capabilities.handleRecoveryPhoneEnabled,
6030
6228
  ...capabilities.guestGatewayBaseUrl === void 0 ? {} : { integrationGuideUrl: new URL("/guest/guide/integration", capabilities.guestGatewayBaseUrl).toString() },
6031
6229
  tenantId: capabilities.tenantId,
6032
- tenantStorageScopeId: this.activeTenant?.storageScopeId,
6230
+ tenantStorageScopeId: this.activeTenant.storageScopeId,
6033
6231
  tenantGeneration: capabilities.generation,
6034
6232
  tenantOnline: capabilities.online,
6035
6233
  services: {
@@ -6039,6 +6237,17 @@ let AwikiService = (() => {
6039
6237
  }
6040
6238
  };
6041
6239
  }
6240
+ /** Explicit discovery retry; ordinary config reads retain their zero-argument contract. */
6241
+ async refreshBackendCapabilities() {
6242
+ this.ensureTenantOptions();
6243
+ const generation = this.runtimeGeneration;
6244
+ await Promise.allSettled([this.refreshTenantCapabilities(true), this.refreshBackendProjection(true)]);
6245
+ if (generation !== this.runtimeGeneration) return {
6246
+ ok: false,
6247
+ error: failure("conflict")
6248
+ };
6249
+ return this.getConfig();
6250
+ }
6042
6251
  /** Trusted same-process capability binding for optional tenant participants. */
6043
6252
  getTenantCapabilities() {
6044
6253
  const capabilities = this.activeCapabilities;
@@ -6046,16 +6255,117 @@ let AwikiService = (() => {
6046
6255
  return { ...capabilities };
6047
6256
  }
6048
6257
  /** Refresh the active tenant's optional service advertisement without affecting Core availability. */
6049
- refreshTenantCapabilities() {
6258
+ refreshTenantCapabilities(force = true) {
6050
6259
  this.ensureTenantOptions();
6051
- return this.discoverTenantCapabilities(1e4);
6260
+ if (this.capabilityDiscoveryTask !== void 0) return this.capabilityDiscoveryTask;
6261
+ if (!force && Date.now() - this.capabilityDiscoveryAt < 3e5 && this.activeCapabilities !== void 0) return Promise.resolve(this.activeCapabilities);
6262
+ const generation = this.runtimeGeneration;
6263
+ const task = this.discoverTenantCapabilities(1e4).finally(() => {
6264
+ if (this.runtimeGeneration === generation && this.capabilityDiscoveryTask === task) {
6265
+ this.capabilityDiscoveryTask = void 0;
6266
+ this.capabilityDiscoveryAt = Date.now();
6267
+ }
6268
+ });
6269
+ this.capabilityDiscoveryTask = task;
6270
+ return task;
6271
+ }
6272
+ refreshBackendProjection(force = false) {
6273
+ const provider = this.provider;
6274
+ if (provider === void 0) return Promise.resolve();
6275
+ if (this.backendFeaturesTask !== void 0) return this.backendFeaturesTask;
6276
+ const revision = this.sessionRevision;
6277
+ const generation = this.runtimeGeneration;
6278
+ if (this.backendFeaturesRevision !== revision) {
6279
+ this.backendFeatures = Object.fromEntries(Object.entries(this.backendFeatures).filter(([key]) => key.includes(".")));
6280
+ this.backendAttachmentLimit = void 0;
6281
+ this.backendDiagnostics = this.backendDiagnostics.public === void 0 ? {} : { public: this.backendDiagnostics.public };
6282
+ this.backendFeaturesRevision = revision;
6283
+ }
6284
+ const current = () => provider === this.provider && revision === this.sessionRevision && generation === this.runtimeGeneration;
6285
+ const task = (async () => {
6286
+ const authenticated = !await this.isSignedOut() && await provider.client.getIdentity() !== null;
6287
+ await Promise.all([false, ...authenticated ? [true] : []].map(async (local) => {
6288
+ try {
6289
+ this.observeBackendProjection(provider, local, current);
6290
+ const snapshot = await provider.client.backendCapabilities?.(local, force);
6291
+ if (!current()) return;
6292
+ if (snapshot === void 0) return;
6293
+ if (this.backendSubscriptions.has(local)) return;
6294
+ this.recordBackendDiagnostics(local, snapshot);
6295
+ if (snapshot.status === "unavailable") return;
6296
+ this.backendFeatures = {
6297
+ ...this.backendFeatures,
6298
+ ...projectBackendSnapshot(snapshot, local)
6299
+ };
6300
+ if (local) this.backendAttachmentLimit = backendAttachmentMaxBytes(snapshot, this.resolved.attachmentMaxBytes);
6301
+ if (this.backendFeatures["agents.managed_agents"] === "unsupported") await this.detachAgentConsumer(provider);
6302
+ else if (this.backendFeatures["agents.managed_agents"] === "supported") this.ensureAgentConsumer(provider);
6303
+ } catch (error) {
6304
+ if (current()) this.ctx.logger("awiki-capabilities").debug("AWiki capability refresh failed: %s", error instanceof AwikiSdkError ? error.code : "unavailable");
6305
+ }
6306
+ }));
6307
+ })().finally(() => {
6308
+ if (this.backendFeaturesTask === task) this.backendFeaturesTask = void 0;
6309
+ });
6310
+ this.backendFeaturesTask = task;
6311
+ task.catch(() => void 0);
6312
+ return task;
6313
+ }
6314
+ recordBackendDiagnostics(local, snapshot) {
6315
+ this.backendDiagnostics = {
6316
+ ...this.backendDiagnostics,
6317
+ [local ? "message" : "public"]: {
6318
+ ...typeof snapshot.fetched_at === "number" ? { fetchedAt: snapshot.fetched_at } : {},
6319
+ ...snapshot.refresh_error === void 0 ? {} : { refreshError: snapshot.refresh_error }
6320
+ }
6321
+ };
6322
+ }
6323
+ stopBackendSubscriptions() {
6324
+ for (const subscription of this.backendSubscriptions.values()) subscription.stop();
6325
+ this.backendSubscriptions.clear();
6326
+ this.backendSubscriptionPending.clear();
6327
+ }
6328
+ observeBackendProjection(provider, local, current) {
6329
+ const subscribe = provider.client.subscribeBackendCapabilities;
6330
+ if (subscribe === void 0 || this.backendSubscriptions.has(local) || this.backendSubscriptionPending.has(local)) return;
6331
+ this.backendSubscriptionPending.add(local);
6332
+ (async () => {
6333
+ const subscription = await subscribe.call(provider.client, local);
6334
+ if (!current()) {
6335
+ subscription.stop();
6336
+ return;
6337
+ }
6338
+ this.backendSubscriptionPending.delete(local);
6339
+ this.backendSubscriptions.set(local, subscription);
6340
+ try {
6341
+ while (current()) {
6342
+ const state = await subscription.nextUpdate();
6343
+ if (state === null || !current()) break;
6344
+ this.recordBackendDiagnostics(local, state.snapshot);
6345
+ if (state.snapshot.status === "unavailable") continue;
6346
+ this.backendFeatures = {
6347
+ ...this.backendFeatures,
6348
+ ...projectBackendSnapshot(state.snapshot, local)
6349
+ };
6350
+ if (local) this.backendAttachmentLimit = backendAttachmentMaxBytes(state.snapshot, this.resolved.attachmentMaxBytes);
6351
+ if (this.backendFeatures["agents.managed_agents"] === "unsupported") await this.detachAgentConsumer(provider);
6352
+ else if (this.backendFeatures["agents.managed_agents"] === "supported") this.ensureAgentConsumer(provider);
6353
+ }
6354
+ } finally {
6355
+ subscription.stop();
6356
+ if (this.backendSubscriptions.get(local) === subscription) this.backendSubscriptions.delete(local);
6357
+ }
6358
+ })().catch(() => {
6359
+ if (current()) this.backendSubscriptionPending.delete(local);
6360
+ });
6052
6361
  }
6053
6362
  async discoverTenantCapabilities(timeoutMs) {
6054
6363
  const options = this.activeClientOptions;
6055
6364
  const tenant = this.activeTenant;
6056
6365
  if (options === void 0 || tenant === void 0) throw new ProviderUnavailableError();
6057
6366
  const generation = this.runtimeGeneration;
6058
- const fallback = {
6367
+ const previous = this.activeCapabilities;
6368
+ const fallback = previous?.tenantId === tenant.tenantId && previous.generation === generation ? { ...previous } : {
6059
6369
  tenantId: tenant.tenantId,
6060
6370
  generation,
6061
6371
  online: false,
@@ -6075,6 +6385,7 @@ let AwikiService = (() => {
6075
6385
  try {
6076
6386
  const endpoint = new URL("/user-service/v1/server-info", options.userServiceUrl);
6077
6387
  endpoint.searchParams.set("client_platform", "dsh");
6388
+ endpoint.searchParams.set("capabilities_version", "1");
6078
6389
  const response = await fetch(endpoint, {
6079
6390
  method: "GET",
6080
6391
  headers: {
@@ -6089,19 +6400,37 @@ let AwikiService = (() => {
6089
6400
  const text = await readBoundedResponseText(response, SERVER_INFO_RESPONSE_MAX_BYTES);
6090
6401
  const value = text === void 0 ? void 0 : JSON.parse(text);
6091
6402
  if (typeof value !== "object" || value === null || value.schema_version !== 1) return publish(fallback);
6403
+ const versioned = value.capabilities_version === 1;
6404
+ if (!versioned) {
6405
+ if ("capabilities_version" in value) return publish({
6406
+ tenantId: tenant.tenantId,
6407
+ generation,
6408
+ online: true,
6409
+ handleRecoveryPhoneEnabled: false
6410
+ });
6411
+ return publish(fallback);
6412
+ }
6092
6413
  const methods = value.identity?.handle_recovery?.methods;
6093
- const handleRecoveryPhoneEnabled = Array.isArray(methods) && methods.some((method) => typeof method === "object" && method !== null && method.id === "phone" && method.enabled === true && method.verification?.required === true && method.verification?.type === "sms_otp");
6094
- const services = value.services;
6414
+ const handleRecoveryPhoneEnabled = versioned && Array.isArray(methods) && methods.some((method) => typeof method === "object" && method !== null && method.id === "phone" && method.enabled === true && method.verification?.required === true && method.verification?.type === "sms_otp");
6415
+ const services = versioned ? value.services : void 0;
6095
6416
  const modelProxyBaseUrl = publishedServiceBaseUrl(services, "model_proxy", this.resolved.allowInsecureLoopbackForTesting);
6096
6417
  const guestGatewayBaseUrl = publishedServiceBaseUrl(services, "guest_gateway", false);
6097
- return publish({
6418
+ const capabilities = {
6098
6419
  tenantId: tenant.tenantId,
6099
6420
  generation,
6100
6421
  online: true,
6101
6422
  handleRecoveryPhoneEnabled,
6102
6423
  ...modelProxyBaseUrl === void 0 ? {} : { modelProxyBaseUrl },
6103
6424
  ...guestGatewayBaseUrl === void 0 ? {} : { guestGatewayBaseUrl }
6104
- });
6425
+ };
6426
+ if (this.runtimeGeneration === generation && this.activeTenant?.tenantId === tenant.tenantId) {
6427
+ if (!new TenantCapabilityCache(options.stateRoot, options.userServiceUrl).write({
6428
+ handleRecoveryPhoneEnabled,
6429
+ ...modelProxyBaseUrl === void 0 ? {} : { modelProxyBaseUrl },
6430
+ ...guestGatewayBaseUrl === void 0 ? {} : { guestGatewayBaseUrl }
6431
+ })) this.ctx.logger("awiki-capabilities").debug("AWiki capability metadata persistence failed");
6432
+ }
6433
+ return publish(capabilities);
6105
6434
  } catch {
6106
6435
  return publish(fallback);
6107
6436
  } finally {
@@ -6192,7 +6521,10 @@ let AwikiService = (() => {
6192
6521
  const session = { status: "signed-out" };
6193
6522
  this.publishSession(session);
6194
6523
  const provider = this.provider;
6195
- if (provider !== void 0) await this.stopProviderRuntime(provider);
6524
+ if (provider !== void 0) {
6525
+ await this.stopProviderRuntime(provider);
6526
+ await this.updateBackendDiscoveryLifecycle(provider, false);
6527
+ }
6196
6528
  return {
6197
6529
  ok: true,
6198
6530
  value: session
@@ -6225,7 +6557,10 @@ let AwikiService = (() => {
6225
6557
  };
6226
6558
  this.publishSession(session);
6227
6559
  const provider = this.provider;
6228
- if (provider !== void 0) this.ensureProviderRuntime(provider);
6560
+ if (provider !== void 0) {
6561
+ this.ensureProviderRuntime(provider);
6562
+ await this.updateBackendDiscoveryLifecycle(provider, true);
6563
+ }
6229
6564
  return {
6230
6565
  ok: true,
6231
6566
  value: session
@@ -6459,6 +6794,8 @@ let AwikiService = (() => {
6459
6794
  const recoveries = await client.listPendingRecoveries();
6460
6795
  const pendingRegistrations = await client.pendingIdentityRegistrations();
6461
6796
  const creationMethods = await client.identityCreationMethods().catch(() => []);
6797
+ const backend = await client.backendCapabilities?.(false);
6798
+ const registrationHandleOnly = backend?.capabilities["registration.handle_only"]?.support === "supported" && (backend.status === "confirmed" || backend.status === "stale");
6462
6799
  const identity = await client.getIdentity();
6463
6800
  const capabilities = identity === null ? void 0 : await client.identityMethodCapabilities(identity.did);
6464
6801
  const domain = this.activeClientOptions?.userServiceDomain;
@@ -6473,6 +6810,7 @@ let AwikiService = (() => {
6473
6810
  },
6474
6811
  joining,
6475
6812
  creationMethods,
6813
+ registrationHandleOnly,
6476
6814
  pendingRegistrations,
6477
6815
  ...capabilities === void 0 ? {} : { methodCapabilities: capabilities },
6478
6816
  recoveries: recoveries.filter((value) => domain !== void 0 && value.fullHandle.slice(value.fullHandle.indexOf(".") + 1) === domain)
@@ -6874,7 +7212,12 @@ let AwikiService = (() => {
6874
7212
  ok: false,
6875
7213
  error: failure("invalid-request")
6876
7214
  };
6877
- const created = await this.run((client) => client.createGroup(normalized.name, normalized.encrypted, normalized.operationId));
7215
+ const created = await this.run(async (client) => {
7216
+ await requireBackendFeature(client, "groups");
7217
+ if (normalized.encrypted) await requireBackendFeature(client, "group_e2ee");
7218
+ if (normalized.members.length > 0) await requireBackendFeature(client, "group_admin_add");
7219
+ return client.createGroup(normalized.name, normalized.encrypted, normalized.operationId);
7220
+ });
6878
7221
  if (!created.ok) return created;
6879
7222
  const addedMembers = [];
6880
7223
  const failedMembers = [];
@@ -6911,7 +7254,10 @@ let AwikiService = (() => {
6911
7254
  ok: false,
6912
7255
  error: failure("invalid-request")
6913
7256
  });
6914
- return this.run((client) => client.repairGroupSecurity(groupDid));
7257
+ return this.run(async (client) => {
7258
+ await requireBackendFeature(client, "group_e2ee");
7259
+ return client.repairGroupSecurity(groupDid);
7260
+ });
6915
7261
  }
6916
7262
  /** Join one open group and return its authoritative membership state. */
6917
7263
  joinGroup(request) {
@@ -6920,7 +7266,10 @@ let AwikiService = (() => {
6920
7266
  ok: false,
6921
7267
  error: failure("invalid-request")
6922
7268
  });
6923
- return this.run((client) => client.joinGroup(groupDid));
7269
+ return this.run(async (client) => {
7270
+ await requireBackendFeature(client, "group_open_join");
7271
+ return client.joinGroup(groupDid);
7272
+ });
6924
7273
  }
6925
7274
  /** Leave one group. Core rejects owner leave and unsupported security profiles. */
6926
7275
  async leaveGroup(request) {
@@ -7502,6 +7851,7 @@ let AwikiService = (() => {
7502
7851
  return client.getLocalDeviceJoinVerificationProgress(notice.joinSessionId);
7503
7852
  }
7504
7853
  async requireDeviceManager(client) {
7854
+ await requireBackendFeature(client, "identity.multi_device");
7505
7855
  const current = await client.getCurrentDeviceSummary();
7506
7856
  if (!current.canManage || current.role !== "admin" || current.readiness !== "admin_ready") throw Object.assign(/* @__PURE__ */ new Error("device manager required"), {
7507
7857
  name: "AwikiSdkError",
@@ -7509,6 +7859,7 @@ let AwikiService = (() => {
7509
7859
  });
7510
7860
  }
7511
7861
  async requireRootTransferRecipient(client, deviceId) {
7862
+ await requireBackendFeature(client, "identity.root_transfer");
7512
7863
  if (!client.trustedUserPresenceSupported) throw Object.assign(/* @__PURE__ */ new Error("trusted user presence unsupported"), {
7513
7864
  name: "AwikiSdkError",
7514
7865
  code: "forbidden"
@@ -7635,6 +7986,7 @@ let AwikiService = (() => {
7635
7986
  }
7636
7987
  /** Invalidate cached session work and cancel every model request still owned by the old session. */
7637
7988
  invalidateSummaries() {
7989
+ this.stopBackendSubscriptions();
7638
7990
  this.sessionRevision += 1;
7639
7991
  this.requestRefs.clear();
7640
7992
  this.requestSessions.clear();
@@ -7718,7 +8070,10 @@ let AwikiService = (() => {
7718
8070
  error: failure("invalid-request")
7719
8071
  });
7720
8072
  }
7721
- return this.run((client) => operation(client, request), options);
8073
+ return this.run(async (client) => {
8074
+ await requireBackendFeature(client, "services.mail");
8075
+ return operation(client, request);
8076
+ }, options);
7722
8077
  }
7723
8078
  /** Read and cache the private Host-owned session marker. */
7724
8079
  async isSignedOut() {
@@ -7772,6 +8127,14 @@ let AwikiService = (() => {
7772
8127
  this.sessionMutation = pending.then(() => void 0, () => void 0);
7773
8128
  return pending;
7774
8129
  }
8130
+ /** Discovery is an optional projection, not authority for a committed session transition. */
8131
+ async updateBackendDiscoveryLifecycle(provider, active) {
8132
+ try {
8133
+ await provider.client.setBackendDiscoveryActive?.(active);
8134
+ } catch {
8135
+ this.ctx.logger("awiki-backend").warn("AWiki backend discovery lifecycle unavailable");
8136
+ }
8137
+ }
7775
8138
  /** Start identity realtime and the optional Agent consumer without blocking identity success. */
7776
8139
  ensureProviderRuntime(provider) {
7777
8140
  if (this.tenantSwitching) return;
@@ -7781,6 +8144,7 @@ let AwikiService = (() => {
7781
8144
  this.replaceProviderRuntime(provider, identityDid);
7782
8145
  return;
7783
8146
  }
8147
+ this.refreshBackendProjection();
7784
8148
  this.ensureRealtimeSupervisor(provider);
7785
8149
  this.ensureAgentConsumer(provider);
7786
8150
  }
@@ -7857,6 +8221,7 @@ let AwikiService = (() => {
7857
8221
  const logger = this.ctx.logger("awiki-listener");
7858
8222
  const observed = (async () => {
7859
8223
  await this.detachAgentConsumer(provider);
8224
+ if ((await projectBackendFeatures(provider.client, false))["agents.managed_agents"] !== "supported") return;
7860
8225
  if (!this.agentConsumerFenceMatches(provider, workspaceContext, identityDid, generation)) return;
7861
8226
  const agents = new DshAwikiListenerAgentRuntime(workspaceContext, this.resolved.listener.workspacePath);
7862
8227
  const consumer = new AwikiAgentListener(source, agents, {