@undefineds.co/xpod 0.4.14 → 0.4.15

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 (202) hide show
  1. package/config/cloud.json +0 -3
  2. package/config/local.json +0 -3
  3. package/config/xpod.base.json +0 -24
  4. package/dist/api/ai-config/AiConfigStore.d.ts +2 -2
  5. package/dist/api/ai-config/AiConfigStore.js +4 -1
  6. package/dist/api/ai-gateway/auth/CallerPodAccess.d.ts +2 -5
  7. package/dist/api/ai-gateway/auth/CallerPodAccess.js +3 -42
  8. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +2 -1
  9. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +1 -1
  10. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +3 -8
  11. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +9 -32
  12. package/dist/api/ai-gateway/connect/index.d.ts +3 -3
  13. package/dist/api/ai-gateway/connect/index.js +9 -38
  14. package/dist/api/ai-gateway/errors.js +2 -1
  15. package/dist/api/ai-gateway/models/PodModelSelectionRepository.d.ts +3 -3
  16. package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +7 -11
  17. package/dist/api/ai-gateway/pod/HostedPodRoute.d.ts +27 -0
  18. package/dist/api/ai-gateway/pod/HostedPodRoute.js +47 -0
  19. package/dist/api/ai-gateway/pod/OwnerPodAccess.d.ts +79 -0
  20. package/dist/api/ai-gateway/pod/OwnerPodAccess.js +192 -0
  21. package/dist/api/ai-gateway/pod/PodInterfaceKeyStore.d.ts +52 -0
  22. package/dist/api/ai-gateway/pod/PodInterfaceKeyStore.js +61 -0
  23. package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.d.ts +3 -3
  24. package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +7 -32
  25. package/dist/api/auth/ClientCredentialsAuthenticator.js +5 -28
  26. package/dist/api/auth/TokenEndpointRoute.d.ts +18 -0
  27. package/dist/api/auth/TokenEndpointRoute.js +36 -0
  28. package/dist/api/chatkit/pod-store.d.ts +4 -4
  29. package/dist/api/chatkit/pod-store.js +24 -95
  30. package/dist/api/container/common.js +36 -23
  31. package/dist/api/container/local.js +62 -29
  32. package/dist/api/container/routes.js +39 -7
  33. package/dist/api/container/types.d.ts +2 -2
  34. package/dist/api/handlers/AiGatewayManagementHandler.d.ts +6 -0
  35. package/dist/api/handlers/AiGatewayManagementHandler.js +17 -4
  36. package/dist/api/handlers/NetworkSettingsHandler.d.ts +7 -0
  37. package/dist/api/handlers/NetworkSettingsHandler.js +7 -0
  38. package/dist/api/handlers/PodSettingsHandler.d.ts +3 -3
  39. package/dist/api/handlers/PodSettingsHandler.js +3 -3
  40. package/dist/api/handlers/ProvisionHandler.d.ts +29 -0
  41. package/dist/api/handlers/ProvisionHandler.js +46 -0
  42. package/dist/api/handlers/ReachabilityHandler.js +3 -1
  43. package/dist/api/runtime.d.ts +9 -0
  44. package/dist/api/runtime.js +16 -3
  45. package/dist/cli/commands/start.d.ts +9 -2
  46. package/dist/cli/commands/start.js +15 -17
  47. package/dist/components/components.jsonld +1 -1
  48. package/dist/components/context.jsonld +5 -42
  49. package/dist/edge/EdgeNodeAgent.d.ts +10 -0
  50. package/dist/edge/EdgeNodeAgent.js +55 -0
  51. package/dist/edge/EdgeNodeAgent.jsonld +8 -0
  52. package/dist/edge/reachability/ReachabilitySessionService.js +4 -1
  53. package/dist/edge/reachability/RouteSetBuilder.d.ts +18 -1
  54. package/dist/edge/reachability/RouteSetBuilder.js +47 -0
  55. package/dist/edge/reachability/RouteSetBuilder.jsonld +29 -0
  56. package/dist/edge/reachability/types.d.ts +8 -2
  57. package/dist/http/SubgraphSparqlHttpHandler.d.ts +1 -25
  58. package/dist/http/SubgraphSparqlHttpHandler.js +0 -100
  59. package/dist/http/SubgraphSparqlHttpHandler.jsonld +0 -8
  60. package/dist/identity/drizzle/PodInterfaceKeyRepository.d.ts +33 -0
  61. package/dist/identity/drizzle/PodInterfaceKeyRepository.js +78 -0
  62. package/dist/identity/drizzle/db.d.ts +7 -0
  63. package/dist/identity/drizzle/db.js +30 -0
  64. package/dist/identity/drizzle/schema.pg.d.ts +9 -0
  65. package/dist/identity/drizzle/schema.pg.js +16 -1
  66. package/dist/identity/drizzle/schema.sqlite.d.ts +106 -0
  67. package/dist/identity/drizzle/schema.sqlite.js +16 -1
  68. package/dist/index.d.ts +1 -2
  69. package/dist/index.js +3 -5
  70. package/dist/runtime/GatewayAdminProxyAuth.d.ts +1 -16
  71. package/dist/runtime/GatewayAdminProxyAuth.js +2 -71
  72. package/dist/runtime/Proxy.d.ts +10 -1
  73. package/dist/runtime/Proxy.js +95 -28
  74. package/dist/runtime/bootstrap.d.ts +10 -0
  75. package/dist/runtime/bootstrap.js +24 -0
  76. package/dist/runtime/host/node/NodeRuntimeHost.js +5 -3
  77. package/dist/runtime/index.d.ts +1 -1
  78. package/dist/runtime/index.js +2 -3
  79. package/dist/runtime/js-runtime.js +10 -3
  80. package/dist/runtime/lifecycle.js +5 -4
  81. package/dist/runtime/port-finder.d.ts +9 -13
  82. package/dist/runtime/port-finder.js +18 -37
  83. package/dist/tunnel/AutoTunnelProvider.d.ts +56 -0
  84. package/dist/tunnel/AutoTunnelProvider.js +181 -0
  85. package/dist/tunnel/index.d.ts +1 -0
  86. package/dist/tunnel/index.js +3 -1
  87. package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
  88. package/node_modules/@undefineds.co/solid-sdk/dist/access-route.cjs +230 -0
  89. package/node_modules/@undefineds.co/solid-sdk/dist/access-route.d.ts +76 -0
  90. package/node_modules/@undefineds.co/solid-sdk/dist/access-route.js +115 -0
  91. package/node_modules/@undefineds.co/solid-sdk/dist/index.cjs +89 -0
  92. package/node_modules/@undefineds.co/solid-sdk/dist/index.d.ts +1 -0
  93. package/node_modules/@undefineds.co/solid-sdk/dist/index.js +1 -0
  94. package/node_modules/@undefineds.co/solid-sdk/package.json +6 -1
  95. package/package.json +2 -2
  96. package/static/app/assets/inrupt-smoke.js +23 -23
  97. package/static/auth-callback/assets/{AiConfigContext-DCXpjzpj.js → AiConfigContext-Bx0HCAW5.js} +1 -1
  98. package/static/auth-callback/assets/{AiConfigPage-fe-xMIXa.js → AiConfigPage-ChQImMbm.js} +1 -1
  99. package/static/auth-callback/assets/{DocumentProcessingPanel-BGnkfMuU.js → DocumentProcessingPanel-D830H4SH.js} +1 -1
  100. package/static/auth-callback/assets/{IndexLifecyclePanel-42Jups0j.js → IndexLifecyclePanel-CnlkjMF8.js} +1 -1
  101. package/static/auth-callback/assets/{IndexSubjectPanel-CYyLOlQU.js → IndexSubjectPanel-CdEfg_CO.js} +1 -1
  102. package/static/auth-callback/assets/{ModelAssignmentsPanel-BOvUH4yr.js → ModelAssignmentsPanel-D-8A8dZx.js} +1 -1
  103. package/static/auth-callback/assets/{ModelsPage-DwyITxt7.js → ModelsPage-Cim7cubp.js} +3 -3
  104. package/static/auth-callback/assets/{NetworkPage-CG9RelJA.js → NetworkPage-CfK3x-pb.js} +2 -2
  105. package/static/auth-callback/assets/{PaneListHeader-yvd5ohs6.js → PaneListHeader-Bf_kEd4U.js} +1 -1
  106. package/static/auth-callback/assets/{SearchIndexingPanel-2_a6ezzj.js → SearchIndexingPanel-4kD4RBEQ.js} +1 -1
  107. package/static/auth-callback/assets/{StatusSubjectPanel-8qtVxnzC.js → StatusSubjectPanel-Bw9AiTSf.js} +1 -1
  108. package/static/auth-callback/assets/{StatusWorkspace-CBdRahCF.js → StatusWorkspace-C-301H-0.js} +1 -1
  109. package/static/auth-callback/assets/{SystemSettingsPage-BZJ6vWkg.js → SystemSettingsPage-BbWn-eG6.js} +1 -1
  110. package/static/auth-callback/assets/{SystemSettingsSubjectPanel-xm08UhXP.js → SystemSettingsSubjectPanel-DA4wvNSm.js} +1 -1
  111. package/static/auth-callback/assets/{XpodAiConnectionsPodStore-V6vgGfuw.js → XpodAiConnectionsPodStore-CohdyLoX.js} +1 -1
  112. package/static/auth-callback/assets/{activity-CeM7xly8.js → activity-CCsb9Oam.js} +1 -1
  113. package/static/auth-callback/assets/ai-connections-DexTPyfC.js +1 -0
  114. package/static/auth-callback/assets/{auth-callback-DTG8cSj7.js → auth-callback-Ds-c62W3.js} +5 -5
  115. package/static/auth-callback/assets/{badge-qJo2Zr4d.js → badge-CQm5OyzR.js} +1 -1
  116. package/static/auth-callback/assets/{collection-runtime-Bztsz31a.js → collection-runtime-DlAuU5TW.js} +1 -1
  117. package/static/auth-callback/assets/{dialog-D-h6x5x0.js → dialog-CnGzg5Ff.js} +1 -1
  118. package/static/auth-callback/assets/{download-C2ir4lmR.js → download-DwMONJvm.js} +1 -1
  119. package/static/auth-callback/assets/{external-link-THLDL4On.js → external-link-Bu5AGqCO.js} +1 -1
  120. package/static/auth-callback/assets/{index-BlVZb9A7.js → index-B0T-1YO8.js} +1 -1
  121. package/static/auth-callback/assets/{index-BRpv4yO3.js → index-C8QZf_4W.js} +1 -1
  122. package/static/auth-callback/assets/{index-D983R1FV.js → index-DwIlrIDG.js} +1 -1
  123. package/static/auth-callback/assets/{index-browser-3FvR0aaf.js → index-browser-lRsMcP_7.js} +1 -1
  124. package/static/auth-callback/assets/{key-round-C9U5j1Ly.js → key-round-BDMtU57P.js} +1 -1
  125. package/static/auth-callback/assets/{rotate-ccw-DP45cD4o.js → rotate-ccw-CoKIGCsQ.js} +1 -1
  126. package/static/auth-callback/assets/{skeleton-Dn4EiSCS.js → skeleton-CPoSWHDr.js} +1 -1
  127. package/static/auth-callback/assets/{waypoints-uqFQ3uEG.js → waypoints-BeYHxqHv.js} +1 -1
  128. package/static/auth-callback/assets/{workspace-layout-DLYK0Wwu.js → workspace-layout-xA5YxiK3.js} +1 -1
  129. package/static/auth-callback/auth-callback.html +1 -1
  130. package/static/dashboard/assets/{AiConfigContext-DJcMECNw.js → AiConfigContext-rWx4TyiH.js} +1 -1
  131. package/static/dashboard/assets/{AiConfigPage-BvE7dOTy.js → AiConfigPage-StVJpDFT.js} +1 -1
  132. package/static/dashboard/assets/{DocumentProcessingPanel-D9780krf.js → DocumentProcessingPanel-DJ5yul2O.js} +1 -1
  133. package/static/dashboard/assets/{IndexLifecyclePanel-SjsUcnbM.js → IndexLifecyclePanel-CpjtgezK.js} +1 -1
  134. package/static/dashboard/assets/{IndexSubjectPanel-C-ym2Tqi.js → IndexSubjectPanel-BVKcot1_.js} +1 -1
  135. package/static/dashboard/assets/{ModelAssignmentsPanel-BRjZ-arB.js → ModelAssignmentsPanel-BxVgWZPc.js} +1 -1
  136. package/static/dashboard/assets/{ModelsPage-BtTU6du3.js → ModelsPage-CECBwmeO.js} +3 -3
  137. package/static/dashboard/assets/{NetworkPage-CV9qpkDg.js → NetworkPage-zQgFscEW.js} +2 -2
  138. package/static/dashboard/assets/{PaneListHeader-KyPTTBsv.js → PaneListHeader-DIPe8pS6.js} +1 -1
  139. package/static/dashboard/assets/{SearchIndexingPanel-DolHGawg.js → SearchIndexingPanel-lalFJ3c8.js} +1 -1
  140. package/static/dashboard/assets/{StatusSubjectPanel-_YZvKiYi.js → StatusSubjectPanel-CpY_KU-G.js} +1 -1
  141. package/static/dashboard/assets/{StatusWorkspace-Dlbh_G40.js → StatusWorkspace-BwWCii3A.js} +1 -1
  142. package/static/dashboard/assets/{SystemSettingsPage-Be7QSIoz.js → SystemSettingsPage-BKZxX-_n.js} +1 -1
  143. package/static/dashboard/assets/{SystemSettingsSubjectPanel-DBp0gja7.js → SystemSettingsSubjectPanel-BZrqb2C5.js} +1 -1
  144. package/static/dashboard/assets/{XpodAiConnectionsPodStore-ClpSjzpP.js → XpodAiConnectionsPodStore-Bbpx9dTN.js} +1 -1
  145. package/static/dashboard/assets/{activity-lsNbTXJu.js → activity-BJ15TLVP.js} +1 -1
  146. package/static/dashboard/assets/ai-connections-GG6qkTJt.js +1 -0
  147. package/static/dashboard/assets/{badge-CVatKjaG.js → badge-BBX5FlaB.js} +1 -1
  148. package/static/dashboard/assets/{collection-runtime-Ec4UnY28.js → collection-runtime-UBbHBzdf.js} +1 -1
  149. package/static/dashboard/assets/{dashboard-BEzsRsvF.js → dashboard-7Y6BHyV9.js} +62 -62
  150. package/static/dashboard/assets/{dialog-BuyelQ4V.js → dialog-QrOJ-hB0.js} +1 -1
  151. package/static/dashboard/assets/{download-B4Nf678F.js → download-DK9x-VOP.js} +1 -1
  152. package/static/dashboard/assets/{external-link-PYv9CvKL.js → external-link-Cnkn87mJ.js} +1 -1
  153. package/static/dashboard/assets/{index-C9XPE-mI.js → index-B1z84it5.js} +1 -1
  154. package/static/dashboard/assets/{index-awr7ulur.js → index-DzjauX3N.js} +1 -1
  155. package/static/dashboard/assets/{index-Cx69bTNh.js → index-cIa8_efI.js} +1 -1
  156. package/static/dashboard/assets/{key-round-VsY80U1Z.js → key-round-BoS6UqKz.js} +1 -1
  157. package/static/dashboard/assets/{rotate-ccw-Cz1K4vE9.js → rotate-ccw-BeWV76i0.js} +1 -1
  158. package/static/dashboard/assets/{skeleton-BhNv_8tO.js → skeleton-CypcJL07.js} +1 -1
  159. package/static/dashboard/assets/{waypoints-DYS9XFYV.js → waypoints-zHOOqfhi.js} +1 -1
  160. package/static/dashboard/assets/{workspace-layout-OZJAFx5V.js → workspace-layout-DZ9JuxnM.js} +1 -1
  161. package/static/dashboard/dashboard.html +1 -1
  162. package/static/settings/assets/{AiConfigContext-B8JS3MXF.js → AiConfigContext-D3b1l_Kw.js} +1 -1
  163. package/static/settings/assets/{AiConfigPage-C3Zl8LUl.js → AiConfigPage-DYKw-atP.js} +1 -1
  164. package/static/settings/assets/{DocumentProcessingPanel-DpDZAQvf.js → DocumentProcessingPanel-DguRfsIV.js} +1 -1
  165. package/static/settings/assets/{IndexLifecyclePanel-Ec1XF88t.js → IndexLifecyclePanel-B85qJJA4.js} +1 -1
  166. package/static/settings/assets/{IndexSubjectPanel-BulT9BmO.js → IndexSubjectPanel-CsN7IxaV.js} +1 -1
  167. package/static/settings/assets/{ModelAssignmentsPanel-BcGSmYjb.js → ModelAssignmentsPanel-DEp9byI6.js} +1 -1
  168. package/static/settings/assets/{ModelsPage-CvTCKy1T.js → ModelsPage-CzTnPs_G.js} +3 -3
  169. package/static/settings/assets/{NetworkPage-DQF5u9Bn.js → NetworkPage-BDVnmUU0.js} +2 -2
  170. package/static/settings/assets/{PaneListHeader-D40hqcVl.js → PaneListHeader-Cpe39N76.js} +1 -1
  171. package/static/settings/assets/{SearchIndexingPanel-iCUPOx_0.js → SearchIndexingPanel-DA9w8lLL.js} +1 -1
  172. package/static/settings/assets/{StatusSubjectPanel-E--ZHSql.js → StatusSubjectPanel-BU0Aq0CS.js} +1 -1
  173. package/static/settings/assets/{StatusWorkspace-YjDAx1II.js → StatusWorkspace-D1AC1i5N.js} +1 -1
  174. package/static/settings/assets/{SystemSettingsPage-CDdV3mC9.js → SystemSettingsPage-ChKvOioO.js} +1 -1
  175. package/static/settings/assets/{SystemSettingsSubjectPanel-Br8WazDq.js → SystemSettingsSubjectPanel-DyTpOnD2.js} +1 -1
  176. package/static/settings/assets/{XpodAiConnectionsPodStore-CLr38P_Z.js → XpodAiConnectionsPodStore-VU5Bsz1a.js} +1 -1
  177. package/static/settings/assets/{activity-C5DAHHTE.js → activity-DGpLfsQB.js} +1 -1
  178. package/static/settings/assets/ai-connections-DexTPyfC.js +1 -0
  179. package/static/settings/assets/{badge-D8eIyxKt.js → badge-_hCrrWRP.js} +1 -1
  180. package/static/settings/assets/{collection-runtime-DLc9zil8.js → collection-runtime-E40TZ8Cl.js} +1 -1
  181. package/static/settings/assets/{dialog-CyuR7Va4.js → dialog-DTr2JkHn.js} +1 -1
  182. package/static/settings/assets/{download-c0oxZrCz.js → download-B_D21DDX.js} +1 -1
  183. package/static/settings/assets/{external-link-D3Z34fP_.js → external-link-BATYaM13.js} +1 -1
  184. package/static/settings/assets/{index-CSr3-2Cj.js → index-BAlAVi-J.js} +1 -1
  185. package/static/settings/assets/{index-ComidxAm.js → index-Ckh4h9jj.js} +1 -1
  186. package/static/settings/assets/{index-CKGbRFlc.js → index-DOiwLF4B.js} +1 -1
  187. package/static/settings/assets/{index-browser-CAscEbae.js → index-browser-DFFWN3Kr.js} +1 -1
  188. package/static/settings/assets/{key-round-DyjLmXuB.js → key-round-ZTVU_1O1.js} +1 -1
  189. package/static/settings/assets/{rotate-ccw-679HF_ec.js → rotate-ccw-DDdw8PTU.js} +1 -1
  190. package/static/settings/assets/{settings-DKknqu4C.js → settings-CyLnISOv.js} +62 -62
  191. package/static/settings/assets/{skeleton-CkopNkPB.js → skeleton-DEzA3hc2.js} +1 -1
  192. package/static/settings/assets/{waypoints-CrqfAftE.js → waypoints-BSdfNvEb.js} +1 -1
  193. package/static/settings/assets/{workspace-layout-UN6c4qTG.js → workspace-layout-BOQnl_3d.js} +1 -1
  194. package/static/settings/settings.html +1 -1
  195. package/dist/api/ai-gateway/pod/HostedPodDataAccess.d.ts +0 -31
  196. package/dist/api/ai-gateway/pod/HostedPodDataAccess.js +0 -330
  197. package/dist/http/InternalPodDataHttpHandler.d.ts +0 -57
  198. package/dist/http/InternalPodDataHttpHandler.js +0 -551
  199. package/dist/http/InternalPodDataHttpHandler.jsonld +0 -243
  200. package/static/auth-callback/assets/ai-connections-C6yaE6WQ.js +0 -1
  201. package/static/dashboard/assets/ai-connections-CXp5gd1J.js +0 -1
  202. package/static/settings/assets/ai-connections-C6yaE6WQ.js +0 -1
@@ -2,7 +2,7 @@ import { getLoggerFor } from 'global-logger-factory';
2
2
  import type { ApiServer } from '../ApiServer';
3
3
  import type { PodLookupRepository } from '../../identity/drizzle/PodLookupRepository';
4
4
  import type { UsageRepository } from '../../storage/quota/UsageRepository';
5
- import type { InternalPodAccessTokenProvider } from '../ai-gateway/pod/HostedPodDataAccess';
5
+ import type { PodAccessFetchProvider } from '../ai-gateway/pod/OwnerPodAccess';
6
6
  export interface PodSettingsStatus {
7
7
  identity: {
8
8
  webId: string;
@@ -57,10 +57,10 @@ export interface PodSettingsHandlerOptions {
57
57
  }
58
58
  export declare function registerPodSettingsRoutes(server: ApiServer, options: PodSettingsHandlerOptions): void;
59
59
  export declare class DrizzlePodAiConnectionsStatusReader implements PodAiConnectionsStatusReader {
60
- private readonly internalPodAccess?;
60
+ private readonly podAccess?;
61
61
  private readonly deployment;
62
62
  private readonly dbFactory;
63
- constructor(internalPodAccess?: InternalPodAccessTokenProvider | undefined, deployment?: string, dbFactory?: (input: {
63
+ constructor(podAccess?: PodAccessFetchProvider | undefined, deployment?: string, dbFactory?: (input: {
64
64
  webId: string;
65
65
  podUrl: string;
66
66
  fetch: typeof fetch;
@@ -77,13 +77,13 @@ async function readStorageStatus(usageRepo, pod) {
77
77
  };
78
78
  }
79
79
  class DrizzlePodAiConnectionsStatusReader {
80
- constructor(internalPodAccess, deployment = 'local', dbFactory = createAiConnectionsStatusDb) {
81
- this.internalPodAccess = internalPodAccess;
80
+ constructor(podAccess, deployment = 'local', dbFactory = createAiConnectionsStatusDb) {
81
+ this.podAccess = podAccess;
82
82
  this.deployment = deployment;
83
83
  this.dbFactory = dbFactory;
84
84
  }
85
85
  async read({ webId, podUrl }) {
86
- const trustedFetch = await this.internalPodAccess?.getTrustedFetch(webId);
86
+ const trustedFetch = await this.podAccess?.getPodFetch(webId);
87
87
  if (!trustedFetch) {
88
88
  return { status: 'unsupported', reason: 'not_configured' };
89
89
  }
@@ -14,6 +14,7 @@
14
14
  * 返回 SP 配置状态,供 Linx 查询
15
15
  */
16
16
  import type { ApiServer } from '../ApiServer';
17
+ import type { AccessRoute } from '../../edge/reachability/types';
17
18
  import type { EdgeNodeRepository } from '../../identity/drizzle/EdgeNodeRepository';
18
19
  import type { ServiceTokenRepositoryPort } from '../../identity/drizzle/ServiceTokenRepository';
19
20
  import type { DdnsRepository } from '../../identity/drizzle/DdnsRepository';
@@ -67,6 +68,25 @@ export interface ProvisionStatusOptions {
67
68
  cloudBaseUrl?: string;
68
69
  /** provisionCode(可选,由环境变量传入) */
69
70
  provisionCode?: string;
71
+ /**
72
+ * Live state of the public route, read per request.
73
+ *
74
+ * A configured tunnel is not the same thing as a reachable public route, and a
75
+ * node whose tunnel is down still owns a canonical URL that same-machine
76
+ * clients reach over the loopback access route (`docs/multi-channel-access.md`).
77
+ * Clients need to tell those apart before they decide whether a canonical
78
+ * request can leave the machine, so this is reported rather than implied.
79
+ */
80
+ readPublicRoute?: () => PublicRouteStatus | Promise<PublicRouteStatus>;
81
+ /**
82
+ * Every access point this node can currently prove it offers, read per request.
83
+ *
84
+ * A canonical URL is one identity reachable over several physical paths, and
85
+ * only the client knows which path it is standing on, so the runtime reports
86
+ * the paths themselves (canonical public route, tunnel, ...) instead of picking
87
+ * one for the client (`docs/multi-channel-access.md`).
88
+ */
89
+ readAccessRoutes?: () => AccessRoute[] | Promise<AccessRoute[]>;
70
90
  /** Persist refreshed local-only setup state to the single local setup file. */
71
91
  persistState?: (state: ProvisionStatusStateUpdate) => Promise<void> | void;
72
92
  /** 测试/调试注入 */
@@ -74,6 +94,15 @@ export interface ProvisionStatusOptions {
74
94
  now?: () => number;
75
95
  refreshGraceSeconds?: number;
76
96
  }
97
+ /** What a client can learn about reaching this node's canonical URL from outside. */
98
+ export interface PublicRouteStatus {
99
+ /** A tunnel provider is configured for this node. */
100
+ configured: boolean;
101
+ /** The provider confirmed a proxy-ready public route. */
102
+ available: boolean;
103
+ provider?: string;
104
+ endpoint?: string;
105
+ }
77
106
  export interface ProvisionStatusStateUpdate {
78
107
  nodeId: string;
79
108
  nodeToken: string;
@@ -442,6 +442,21 @@ function readManagedTunnelConfig(metadata) {
442
442
  },
443
443
  };
444
444
  }
445
+ /**
446
+ * Only the facts a client needs to rank and use an access point are reported:
447
+ * where it points, how it is reached and whether it is currently known to work.
448
+ */
449
+ function reportableAccessRoutes(routes) {
450
+ return (routes ?? []).map((route) => ({
451
+ id: route.id,
452
+ kind: route.kind,
453
+ targetUrl: route.targetUrl,
454
+ priority: route.priority,
455
+ requiresManagedClient: route.requiresManagedClient,
456
+ visibility: route.visibility,
457
+ health: route.health,
458
+ }));
459
+ }
445
460
  function registerProvisionStatusRoute(server, options) {
446
461
  const logger = (0, global_logger_factory_1.getLoggerFor)('ProvisionStatusHandler');
447
462
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -479,6 +494,37 @@ function registerProvisionStatusRoute(server, options) {
479
494
  body.cloudUrl = options.cloudUrl;
480
495
  body.oidcIssuer = normalizeUrl(options.cloudBaseUrl);
481
496
  }
497
+ // Whether the canonical URL is reachable from outside is a separate fact
498
+ // from whether it exists. A failed read is reported as unavailable, never as
499
+ // available, because guessing here would send a same-machine client to a
500
+ // public route that does not answer.
501
+ if (options.readPublicRoute) {
502
+ let publicRoute;
503
+ try {
504
+ publicRoute = await options.readPublicRoute();
505
+ }
506
+ catch (error) {
507
+ logger.warn(`Failed to read public route state: ${error}`);
508
+ }
509
+ body.publicRoute = {
510
+ configured: Boolean(publicRoute?.configured),
511
+ available: Boolean(publicRoute?.available),
512
+ ...(publicRoute?.provider ? { provider: publicRoute.provider } : {}),
513
+ ...(publicRoute?.endpoint ? { endpoint: publicRoute.endpoint } : {}),
514
+ };
515
+ }
516
+ // Access points are reported as data: which one is best depends on where the
517
+ // client stands, which only the client knows.
518
+ if (options.readAccessRoutes) {
519
+ let routes;
520
+ try {
521
+ routes = await options.readAccessRoutes();
522
+ }
523
+ catch (error) {
524
+ logger.warn(`Failed to read access routes: ${error}`);
525
+ }
526
+ body.routes = reportableAccessRoutes(routes);
527
+ }
482
528
  if (registered) {
483
529
  const canRefresh = canRefreshProvisionStatus(options, state);
484
530
  const currentNow = now();
@@ -214,7 +214,9 @@ function resolveAccess(request, nodeId) {
214
214
  if (auth.nodeId !== nodeId) {
215
215
  return { allowed: false, status: 403, error: 'Node token cannot access another node' };
216
216
  }
217
- return { allowed: true, audience: 'managed' };
217
+ // The node asking about itself stands on its own host, which is the one place its
218
+ // `local-only` loopback access point is usable (`docs/multi-channel-access.md`).
219
+ return { allowed: true, audience: 'local' };
218
220
  }
219
221
  if ((0, AuthContext_1.isServiceAuth)(auth)) {
220
222
  return { allowed: true, audience: 'managed' };
@@ -26,5 +26,14 @@ export declare function autoProvisionFirstRunLocal(config: ApiContainerConfig, l
26
26
  * would let forwarded remote requests inherit local trust. Falls back to the
27
27
  * historical gateway port only when no ingress listener was provisioned.
28
28
  */
29
+ /**
30
+ * The port a remote tunnel forwards to: the Gateway's tunnel entry.
31
+ *
32
+ * The runtime records the listener it actually bound in `XPOD_GATEWAY_INGRESS_PORT`, and the
33
+ * Gateway's own port is only the fallback for an embedding that started no tunnel entry. The
34
+ * gate is structural - anything arriving on that listener is remote whatever it claims - so a
35
+ * tunnel pointed at the Gateway port by mistake still has the forwarding-header check behind
36
+ * it rather than inheriting local trust.
37
+ */
29
38
  export declare function resolveTunnelIngressPort(env?: NodeJS.ProcessEnv): number;
30
39
  export declare function startApiService(options?: StartApiServiceOptions): Promise<ApiServiceHandle>;
@@ -323,10 +323,23 @@ async function reconcileLocalOwnerRoles(container, config, logger) {
323
323
  * would let forwarded remote requests inherit local trust. Falls back to the
324
324
  * historical gateway port only when no ingress listener was provisioned.
325
325
  */
326
+ /**
327
+ * The port a remote tunnel forwards to: the Gateway's tunnel entry.
328
+ *
329
+ * The runtime records the listener it actually bound in `XPOD_GATEWAY_INGRESS_PORT`, and the
330
+ * Gateway's own port is only the fallback for an embedding that started no tunnel entry. The
331
+ * gate is structural - anything arriving on that listener is remote whatever it claims - so a
332
+ * tunnel pointed at the Gateway port by mistake still has the forwarding-header check behind
333
+ * it rather than inheriting local trust.
334
+ */
326
335
  function resolveTunnelIngressPort(env = process.env) {
327
- const raw = env.XPOD_GATEWAY_INGRESS_PORT ?? env.XPOD_MAIN_PORT ?? env.CSS_PORT ?? env.PORT ?? '3000';
328
- const parsed = Number.parseInt(raw, 10);
329
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 3000;
336
+ for (const value of [env.XPOD_GATEWAY_INGRESS_PORT, env.XPOD_MAIN_PORT, env.XPOD_PORT, env.PORT]) {
337
+ const parsed = Number.parseInt(value ?? '', 10);
338
+ if (Number.isFinite(parsed) && parsed > 0) {
339
+ return parsed;
340
+ }
341
+ }
342
+ return 3000;
330
343
  }
331
344
  async function startBackgroundServices(container, logger) {
332
345
  try {
@@ -17,12 +17,19 @@ export declare const startCommand: CommandModule<object, StartArgs>;
17
17
  * port → an OS-assigned loopback port. The first two are strict, because a tunnel that
18
18
  * forwards to a specific port cannot follow us somewhere else.
19
19
  */
20
- export declare function resolveIngressPort(config: {
20
+ /**
21
+ * The port a remote tunnel forwards to: the Gateway's tunnel entry.
22
+ *
23
+ * It is derived from the Gateway port by the same rule the runtime uses, so the CLI can hand
24
+ * it to the managed edge agent before the runtime binds it. A console that forwards somewhere
25
+ * else is a mismatch the tunnel provider reports from its own read-back - the runtime never
26
+ * moves its gate to match a console.
27
+ */
28
+ export declare function resolveIngressPort(provisionedConfig: {
21
29
  tunnelProfiles?: Array<{
22
30
  id: string;
23
31
  provider: string;
24
32
  credentialEnvKey?: string;
25
- credentialConfigured?: boolean;
26
33
  }>;
27
34
  tunnelActiveProfileId?: string;
28
35
  }, mainPort: number): Promise<number>;
@@ -239,30 +239,28 @@ exports.startCommand = {
239
239
  * port → an OS-assigned loopback port. The first two are strict, because a tunnel that
240
240
  * forwards to a specific port cannot follow us somewhere else.
241
241
  */
242
- async function resolveIngressPort(config, mainPort) {
243
- const explicit = process.env.XPOD_GATEWAY_INGRESS_PORT?.trim();
244
- if (explicit) {
245
- return await (0, runtime_1.requireFreePortForWildcard)(Number.parseInt(explicit, 10));
246
- }
247
- const active = config.tunnelProfiles?.find((profile) => profile.id === config.tunnelActiveProfileId)
248
- ?? config.tunnelProfiles?.find((profile) => profile.provider === 'sakura_frp');
242
+ /**
243
+ * The port a remote tunnel forwards to: the Gateway's tunnel entry.
244
+ *
245
+ * It is derived from the Gateway port by the same rule the runtime uses, so the CLI can hand
246
+ * it to the managed edge agent before the runtime binds it. A console that forwards somewhere
247
+ * else is a mismatch the tunnel provider reports from its own read-back - the runtime never
248
+ * moves its gate to match a console.
249
+ */
250
+ async function resolveIngressPort(provisionedConfig, mainPort) {
251
+ const port = await (0, runtime_1.findGatewayIngressPort)(mainPort);
252
+ const active = provisionedConfig.tunnelProfiles?.find((profile) => profile.id === provisionedConfig.tunnelActiveProfileId) ?? provisionedConfig.tunnelProfiles?.find((profile) => profile.provider === 'sakura_frp');
249
253
  if (active?.provider === 'sakura_frp') {
250
254
  const credential = active.credentialEnvKey
251
255
  ? process.env[active.credentialEnvKey] ?? process.env.SAKURA_TUNNEL_TOKEN
252
256
  : process.env.SAKURA_TUNNEL_TOKEN;
253
257
  const assigned = await (0, SakuraFrpTunnelProvider_1.resolveSakuraAssignedLocalPort)(credential);
254
- if (assigned && assigned !== mainPort) {
255
- return await (0, runtime_1.requireFreePortForWildcard)(assigned);
258
+ if (assigned !== undefined && assigned !== port) {
259
+ (0, global_logger_factory_1.getLoggerFor)('XpodStart').warn(`The Sakura console forwards to local port ${assigned}, but this runtime's tunnel entry is ${port}; `
260
+ + `update the tunnel's local port in the console to ${port}`);
256
261
  }
257
262
  }
258
- // The operator copies this address into a provider console, so it has to survive
259
- // restarts instead of being a fresh OS-assigned port every time.
260
- const stateFile = path_1.default.join(process.cwd(), '.xpod', 'runtime', 'ingress-port');
261
- const stable = await (0, runtime_1.resolveStableLoopbackPort)(stateFile);
262
- if (stable.changed) {
263
- (0, global_logger_factory_1.getLoggerFor)('XpodStart').warn(`The ingress port changed to ${stable.port}; update the tunnel console if it still forwards to the previous port`);
264
- }
265
- return stable.port;
263
+ return port;
266
264
  }
267
265
  function resolveCliOidcIssuer(env, provisionedIssuer, edition) {
268
266
  return (0, oidc_issuer_1.resolveExternalOidcIssuer)(env)
@@ -31,7 +31,6 @@
31
31
  "undefineds:dist/logging/ConfigurableLoggerFactory.jsonld",
32
32
  "undefineds:dist/storage/SparqlUpdateResourceStore.jsonld",
33
33
  "undefineds:dist/http/SubgraphSparqlHttpHandler.jsonld",
34
- "undefineds:dist/http/InternalPodDataHttpHandler.jsonld",
35
34
  "undefineds:dist/http/quota/QuotaAdminHttpHandler.jsonld",
36
35
  "undefineds:dist/http/ClusterIngressRouter.jsonld",
37
36
  "undefineds:dist/http/ClusterWebSocketConfigurator.jsonld",
@@ -134,6 +133,7 @@
134
133
  "undefineds:dist/api/reconciler/WakeAgentQueue.jsonld",
135
134
  "undefineds:dist/api/reconciler/ServerGroupReconcilerService.jsonld",
136
135
  "undefineds:dist/edge/reachability/types.jsonld",
136
+ "undefineds:dist/edge/reachability/RouteSetBuilder.jsonld",
137
137
  "undefineds:dist/edge/reachability/ReachabilitySessionService.jsonld",
138
138
  "undefineds:dist/edge/reachability/ManagedRouteSelector.jsonld",
139
139
  "undefineds:dist/edge/reachability/CanonicalFetch.jsonld",
@@ -1111,48 +1111,6 @@
1111
1111
  }
1112
1112
  }
1113
1113
  },
1114
- "InternalPodDataHttpHandler": {
1115
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler",
1116
- "@prefix": true,
1117
- "@context": {
1118
- "options_resourceStore": {
1119
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_resourceStore"
1120
- },
1121
- "options_gatewayAdminProxyAuthSecret": {
1122
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_gatewayAdminProxyAuthSecret"
1123
- },
1124
- "options_patchBodyParser": {
1125
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_patchBodyParser"
1126
- },
1127
- "options_sparqlHandler": {
1128
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_sparqlHandler"
1129
- },
1130
- "options_baseUrl": {
1131
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_baseUrl"
1132
- },
1133
- "options_basePath": {
1134
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_basePath"
1135
- },
1136
- "resourceStore": {
1137
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_resourceStore"
1138
- },
1139
- "gatewayAdminProxyAuthSecret": {
1140
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_gatewayAdminProxyAuthSecret"
1141
- },
1142
- "patchBodyParser": {
1143
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_patchBodyParser"
1144
- },
1145
- "sparqlHandler": {
1146
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_sparqlHandler"
1147
- },
1148
- "baseUrl": {
1149
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_baseUrl"
1150
- },
1151
- "basePath": {
1152
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler_options_basePath"
1153
- }
1154
- }
1155
- },
1156
1114
  "QuotaAdminHttpHandler": {
1157
1115
  "@id": "undefineds:dist/http/quota/QuotaAdminHttpHandler.jsonld#QuotaAdminHttpHandler",
1158
1116
  "@prefix": true,
@@ -4024,6 +3982,11 @@
4024
3982
  "@prefix": true,
4025
3983
  "@context": {}
4026
3984
  },
3985
+ "BuildPublicAccessRoutesOptions": {
3986
+ "@id": "undefineds:dist/edge/reachability/RouteSetBuilder.jsonld#BuildPublicAccessRoutesOptions",
3987
+ "@prefix": true,
3988
+ "@context": {}
3989
+ },
4027
3990
  "ReachabilitySessionService": {
4028
3991
  "@id": "undefineds:dist/edge/reachability/ReachabilitySessionService.jsonld#ReachabilitySessionService",
4029
3992
  "@prefix": true,
@@ -91,6 +91,16 @@ export declare class EdgeNodeAgent {
91
91
  private recordWithContent;
92
92
  private collectSystemMetrics;
93
93
  private buildHeartbeatMetadata;
94
+ /**
95
+ * Same-machine access point: the local runtime the agent heartbeats for.
96
+ *
97
+ * It is published as `local-only`, so it never reaches public route discovery -
98
+ * a loopback address is meaningless on any other machine - while managed
99
+ * clients still see it first (priority 10). The canonical URL stays canonical:
100
+ * loopback is only its access route.
101
+ */
102
+ private buildLoopbackHeartbeatRoute;
103
+ private resolveLoopbackTargetUrl;
94
104
  private buildLanHeartbeatRoutes;
95
105
  private buildLanHeartbeatRoute;
96
106
  private buildP2PHeartbeatRoute;
@@ -229,6 +229,13 @@ class EdgeNodeAgent {
229
229
  }
230
230
  buildHeartbeatMetadata(options) {
231
231
  const metadata = { ...(options.metadata ?? {}) };
232
+ // Every access point this node has is reported, not just the public one:
233
+ // clients pick the best path for where they are (`docs/multi-channel-access.md`),
234
+ // and a same-machine client needs the loopback entry to do that.
235
+ const loopbackRoute = this.buildLoopbackHeartbeatRoute(options);
236
+ if (loopbackRoute) {
237
+ metadata.routes = this.mergeHeartbeatRoutes(metadata.routes, [loopbackRoute]);
238
+ }
232
239
  const lanRoutes = this.buildLanHeartbeatRoutes(options);
233
240
  if (lanRoutes.length > 0) {
234
241
  metadata.routes = this.mergeHeartbeatRoutes(metadata.routes, lanRoutes);
@@ -239,6 +246,54 @@ class EdgeNodeAgent {
239
246
  }
240
247
  return metadata;
241
248
  }
249
+ /**
250
+ * Same-machine access point: the local runtime the agent heartbeats for.
251
+ *
252
+ * It is published as `local-only`, so it never reaches public route discovery -
253
+ * a loopback address is meaningless on any other machine - while managed
254
+ * clients still see it first (priority 10). The canonical URL stays canonical:
255
+ * loopback is only its access route.
256
+ */
257
+ buildLoopbackHeartbeatRoute(options) {
258
+ const target = options.p2p?.targetBaseUrl;
259
+ const targetUrl = this.resolveLoopbackTargetUrl(target);
260
+ if (!targetUrl) {
261
+ return undefined;
262
+ }
263
+ return {
264
+ id: 'loopback',
265
+ nodeId: options.nodeId,
266
+ ...(options.baseUrl ? { canonicalUrl: options.baseUrl } : {}),
267
+ kind: 'loopback',
268
+ targetUrl,
269
+ priority: 10,
270
+ requiresManagedClient: true,
271
+ visibility: 'local-only',
272
+ health: 'healthy',
273
+ metadata: {
274
+ source: 'local-runtime',
275
+ },
276
+ };
277
+ }
278
+ resolveLoopbackTargetUrl(target) {
279
+ if (!target) {
280
+ return undefined;
281
+ }
282
+ try {
283
+ const url = target instanceof URL ? target : new URL(String(target));
284
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
285
+ return undefined;
286
+ }
287
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/gu, '');
288
+ if (hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '::1') {
289
+ return undefined;
290
+ }
291
+ return url.href.endsWith('/') ? url.href : `${url.href}/`;
292
+ }
293
+ catch {
294
+ return undefined;
295
+ }
296
+ }
242
297
  buildLanHeartbeatRoutes(options) {
243
298
  const p2p = options.p2p;
244
299
  if (!p2p || this.normalizeBoolean(p2p.enabled) === false) {
@@ -212,6 +212,14 @@
212
212
  "@id": "undefineds:dist/edge/EdgeNodeAgent.jsonld#EdgeNodeAgent__member_buildHeartbeatMetadata",
213
213
  "memberFieldName": "buildHeartbeatMetadata"
214
214
  },
215
+ {
216
+ "@id": "undefineds:dist/edge/EdgeNodeAgent.jsonld#EdgeNodeAgent__member_buildLoopbackHeartbeatRoute",
217
+ "memberFieldName": "buildLoopbackHeartbeatRoute"
218
+ },
219
+ {
220
+ "@id": "undefineds:dist/edge/EdgeNodeAgent.jsonld#EdgeNodeAgent__member_resolveLoopbackTargetUrl",
221
+ "memberFieldName": "resolveLoopbackTargetUrl"
222
+ },
215
223
  {
216
224
  "@id": "undefineds:dist/edge/EdgeNodeAgent.jsonld#EdgeNodeAgent__member_buildLanHeartbeatRoutes",
217
225
  "memberFieldName": "buildLanHeartbeatRoutes"
@@ -38,7 +38,10 @@ class ReachabilitySessionService {
38
38
  throw new P2PCandidateSessionLimitExceededError('P2P candidate session limit exceeded');
39
39
  }
40
40
  const routeSet = (0, RouteSetBuilder_1.buildRouteSet)(source, {
41
- audience: 'managed',
41
+ // The node publishes its own access points, loopback included: a client on this host
42
+ // reaches the node there, and a client elsewhere ranks the path out by probing it
43
+ // (`docs/multi-channel-access.md`).
44
+ audience: 'local',
42
45
  baseStorageDomain: this.options.baseStorageDomain,
43
46
  now: createdAt,
44
47
  });
@@ -1,2 +1,19 @@
1
- import type { BuildRouteSetOptions, BuildRouteSetSource, RouteSet } from './types';
1
+ import type { AccessRoute, BuildRouteSetOptions, BuildRouteSetSource, RouteSet } from './types';
2
2
  export declare function buildRouteSet(source: BuildRouteSetSource, options?: BuildRouteSetOptions): RouteSet;
3
+ export interface BuildPublicAccessRoutesOptions {
4
+ /** The node's canonical public URL, when it has one. */
5
+ canonicalUrl?: string;
6
+ /** A provider confirmed the canonical URL is proxy-ready right now. */
7
+ publicRouteAvailable?: boolean;
8
+ /** Endpoint of the user's own tunnel, when one is connected. */
9
+ tunnelEndpoint?: string;
10
+ }
11
+ /**
12
+ * Access points a node can prove from its own configuration: the canonical public
13
+ * URL and the user's tunnel endpoint.
14
+ *
15
+ * Loopback and LAN are deliberately absent: whether either works depends on where
16
+ * the client stands, so the client derives them from its own position instead of
17
+ * being told about paths it may not have (`docs/multi-channel-access.md`).
18
+ */
19
+ export declare function buildPublicAccessRoutes(options: BuildPublicAccessRoutesOptions): AccessRoute[];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildRouteSet = buildRouteSet;
4
+ exports.buildPublicAccessRoutes = buildPublicAccessRoutes;
4
5
  const DEFAULT_PRIORITY = {
5
6
  loopback: 10,
6
7
  lan: 20,
@@ -34,6 +35,44 @@ function buildRouteSet(source, options = {}) {
34
35
  routes,
35
36
  };
36
37
  }
38
+ /**
39
+ * Access points a node can prove from its own configuration: the canonical public
40
+ * URL and the user's tunnel endpoint.
41
+ *
42
+ * Loopback and LAN are deliberately absent: whether either works depends on where
43
+ * the client stands, so the client derives them from its own position instead of
44
+ * being told about paths it may not have (`docs/multi-channel-access.md`).
45
+ */
46
+ function buildPublicAccessRoutes(options) {
47
+ const canonicalUrl = normalizeUrlString(options.canonicalUrl);
48
+ const tunnelEndpoint = normalizeUrlString(options.tunnelEndpoint);
49
+ const routes = [];
50
+ if (canonicalUrl) {
51
+ routes.push({
52
+ id: 'public-direct',
53
+ kind: 'public-direct',
54
+ canonicalUrl,
55
+ targetUrl: canonicalUrl,
56
+ priority: DEFAULT_PRIORITY['public-direct'],
57
+ requiresManagedClient: false,
58
+ visibility: 'public',
59
+ health: options.publicRouteAvailable ? 'healthy' : 'unknown',
60
+ });
61
+ }
62
+ if (tunnelEndpoint && tunnelEndpoint !== canonicalUrl) {
63
+ routes.push({
64
+ id: 'user-tunnel',
65
+ kind: 'user-tunnel',
66
+ canonicalUrl: canonicalUrl ?? tunnelEndpoint,
67
+ targetUrl: tunnelEndpoint,
68
+ priority: DEFAULT_PRIORITY['user-tunnel'],
69
+ requiresManagedClient: false,
70
+ visibility: 'public',
71
+ health: 'healthy',
72
+ });
73
+ }
74
+ return routes;
75
+ }
37
76
  function routesFromMetadata(source, canonicalUrl, metadata) {
38
77
  const rawRoutes = Array.isArray(metadata.routes) ? metadata.routes : [];
39
78
  return rawRoutes
@@ -165,6 +204,14 @@ function defaultVisibility(kind) {
165
204
  return PUBLIC_KINDS.has(kind) ? 'public' : 'authorized-client';
166
205
  }
167
206
  function isVisibleToAudience(route, audience) {
207
+ if (audience === 'local') {
208
+ return true;
209
+ }
210
+ // `local-only` means loopback: usable on the node's own host and nowhere else,
211
+ // so it is never offered to a client on another machine.
212
+ if (route.visibility === 'local-only') {
213
+ return false;
214
+ }
168
215
  if (audience === 'managed') {
169
216
  return true;
170
217
  }
@@ -0,0 +1,29 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@undefineds.co/xpod/^0.0.0/components/context.jsonld"
4
+ ],
5
+ "@id": "npmd:@undefineds.co/xpod",
6
+ "components": [
7
+ {
8
+ "@id": "undefineds:dist/edge/reachability/RouteSetBuilder.jsonld#BuildPublicAccessRoutesOptions",
9
+ "@type": "AbstractClass",
10
+ "requireElement": "BuildPublicAccessRoutesOptions",
11
+ "parameters": [],
12
+ "memberFields": [
13
+ {
14
+ "@id": "undefineds:dist/edge/reachability/RouteSetBuilder.jsonld#BuildPublicAccessRoutesOptions__member_canonicalUrl",
15
+ "memberFieldName": "canonicalUrl"
16
+ },
17
+ {
18
+ "@id": "undefineds:dist/edge/reachability/RouteSetBuilder.jsonld#BuildPublicAccessRoutesOptions__member_publicRouteAvailable",
19
+ "memberFieldName": "publicRouteAvailable"
20
+ },
21
+ {
22
+ "@id": "undefineds:dist/edge/reachability/RouteSetBuilder.jsonld#BuildPublicAccessRoutesOptions__member_tunnelEndpoint",
23
+ "memberFieldName": "tunnelEndpoint"
24
+ }
25
+ ],
26
+ "constructorArguments": []
27
+ }
28
+ ]
29
+ }
@@ -3,7 +3,8 @@ export type AccessRouteVisibility = 'local-only' | 'same-account' | 'authorized-
3
3
  export type AccessRouteHealth = 'unknown' | 'healthy' | 'degraded' | 'unreachable';
4
4
  export interface AccessRoute {
5
5
  id: string;
6
- nodeId: string;
6
+ /** Absent while the node has no identity yet, as on a standalone Local runtime. */
7
+ nodeId?: string;
7
8
  canonicalUrl: string;
8
9
  kind: AccessRouteKind;
9
10
  targetUrl: string;
@@ -21,7 +22,12 @@ export interface RouteSet {
21
22
  generatedAt: string;
22
23
  routes: AccessRoute[];
23
24
  }
24
- export type RouteAudience = 'public' | 'managed';
25
+ /**
26
+ * Who is asking for routes. `local` is the node's own host, which is the only
27
+ * place a loopback access point means anything; `managed` is an authorized
28
+ * client elsewhere, and `public` is unauthenticated discovery.
29
+ */
30
+ export type RouteAudience = 'public' | 'managed' | 'local';
25
31
  export interface BuildRouteSetSource {
26
32
  nodeId: string;
27
33
  canonicalUrl?: string;