@undefineds.co/xpod 0.3.31 → 0.3.32

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 (135) hide show
  1. package/dist/api/auth/AuthContext.d.ts +3 -2
  2. package/dist/api/auth/AuthContext.js +2 -1
  3. package/dist/api/auth/AuthContext.js.map +1 -1
  4. package/dist/api/auth/ClientCredentialsAuthenticator.d.ts +2 -12
  5. package/dist/api/auth/ClientCredentialsAuthenticator.js +4 -4
  6. package/dist/api/auth/ClientCredentialsAuthenticator.js.map +1 -1
  7. package/dist/api/auth/ServiceTokenAuthenticator.d.ts +2 -2
  8. package/dist/api/auth/ServiceTokenAuthenticator.js.map +1 -1
  9. package/dist/api/container/business-token.d.ts +1 -1
  10. package/dist/api/container/business-token.js +5 -1
  11. package/dist/api/container/business-token.js.map +1 -1
  12. package/dist/api/container/common.js +14 -10
  13. package/dist/api/container/common.js.map +1 -1
  14. package/dist/api/container/routes.js +16 -3
  15. package/dist/api/container/routes.js.map +1 -1
  16. package/dist/api/container/types.d.ts +2 -4
  17. package/dist/api/container/types.js.map +1 -1
  18. package/dist/api/handlers/ChatHandler.d.ts +1 -1
  19. package/dist/api/handlers/ChatHandler.js +1 -1
  20. package/dist/api/handlers/ChatHandler.js.map +1 -1
  21. package/dist/api/handlers/EdgeNodeSignalHandler.js +3 -1
  22. package/dist/api/handlers/EdgeNodeSignalHandler.js.map +1 -1
  23. package/dist/api/handlers/PodManagementHandler.d.ts +2 -0
  24. package/dist/api/handlers/PodManagementHandler.js +114 -12
  25. package/dist/api/handlers/PodManagementHandler.js.map +1 -1
  26. package/dist/api/handlers/ProvisionHandler.d.ts +27 -0
  27. package/dist/api/handlers/ProvisionHandler.js +339 -32
  28. package/dist/api/handlers/ProvisionHandler.js.map +1 -1
  29. package/dist/api/handlers/QuotaHandler.js +0 -12
  30. package/dist/api/handlers/QuotaHandler.js.map +1 -1
  31. package/dist/api/handlers/index.d.ts +0 -1
  32. package/dist/api/handlers/index.js +0 -1
  33. package/dist/api/handlers/index.js.map +1 -1
  34. package/dist/api/runtime.js +3 -3
  35. package/dist/api/runtime.js.map +1 -1
  36. package/dist/components/context.jsonld +12 -0
  37. package/dist/edge/EdgeNodeAgent.d.ts +1 -1
  38. package/dist/edge/EdgeNodeAgent.js +1 -1
  39. package/dist/edge/EdgeNodeAgent.js.map +1 -1
  40. package/dist/edge/EdgeNodeDnsCoordinator.d.ts +1 -0
  41. package/dist/edge/EdgeNodeDnsCoordinator.js +9 -3
  42. package/dist/edge/EdgeNodeDnsCoordinator.js.map +1 -1
  43. package/dist/edge/EdgeNodeDnsCoordinator.jsonld +4 -0
  44. package/dist/edge/EdgeNodeHealthProbeService.d.ts +3 -0
  45. package/dist/edge/EdgeNodeHealthProbeService.js +22 -2
  46. package/dist/edge/EdgeNodeHealthProbeService.js.map +1 -1
  47. package/dist/edge/EdgeNodeHealthProbeService.jsonld +12 -0
  48. package/dist/http/ClusterIngressRouter.js +6 -3
  49. package/dist/http/ClusterIngressRouter.js.map +1 -1
  50. package/dist/http/ClusterWebSocketConfigurator.js +6 -2
  51. package/dist/http/ClusterWebSocketConfigurator.js.map +1 -1
  52. package/dist/http/EdgeNodeDirectDebugHttpHandler.d.ts +2 -0
  53. package/dist/http/EdgeNodeDirectDebugHttpHandler.js +18 -3
  54. package/dist/http/EdgeNodeDirectDebugHttpHandler.js.map +1 -1
  55. package/dist/http/EdgeNodeDirectDebugHttpHandler.jsonld +8 -0
  56. package/dist/http/EdgeNodeProxyHttpHandler.js +6 -2
  57. package/dist/http/EdgeNodeProxyHttpHandler.js.map +1 -1
  58. package/dist/http/cluster/PodMigrationHttpHandler.d.ts +2 -2
  59. package/dist/http/cluster/PodMigrationHttpHandler.js +2 -2
  60. package/dist/http/cluster/PodMigrationHttpHandler.js.map +1 -1
  61. package/dist/http/quota/QuotaAdminHttpHandler.js +27 -21
  62. package/dist/http/quota/QuotaAdminHttpHandler.js.map +1 -1
  63. package/dist/identity/drizzle/AccountRepository.d.ts +4 -22
  64. package/dist/identity/drizzle/AccountRepository.js +9 -113
  65. package/dist/identity/drizzle/AccountRepository.js.map +1 -1
  66. package/dist/identity/drizzle/AccountRoleRepository.d.ts +5 -5
  67. package/dist/identity/drizzle/AccountRoleRepository.js +204 -97
  68. package/dist/identity/drizzle/AccountRoleRepository.js.map +1 -1
  69. package/dist/identity/drizzle/DdnsRepository.d.ts +5 -20
  70. package/dist/identity/drizzle/DdnsRepository.js +13 -49
  71. package/dist/identity/drizzle/DdnsRepository.js.map +1 -1
  72. package/dist/identity/drizzle/EdgeNodeRepository.d.ts +13 -6
  73. package/dist/identity/drizzle/EdgeNodeRepository.js +167 -66
  74. package/dist/identity/drizzle/EdgeNodeRepository.js.map +1 -1
  75. package/dist/identity/drizzle/PodLookupRepository.d.ts +7 -36
  76. package/dist/identity/drizzle/PodLookupRepository.js +103 -126
  77. package/dist/identity/drizzle/PodLookupRepository.js.map +1 -1
  78. package/dist/identity/drizzle/ServiceTokenRepository.d.ts +13 -1
  79. package/dist/identity/drizzle/ServiceTokenRepository.js +7 -0
  80. package/dist/identity/drizzle/ServiceTokenRepository.js.map +1 -1
  81. package/dist/identity/drizzle/db.d.ts +2 -1
  82. package/dist/identity/drizzle/db.js +173 -297
  83. package/dist/identity/drizzle/db.js.map +1 -1
  84. package/dist/identity/drizzle/schema.pg.d.ts +3 -11
  85. package/dist/identity/drizzle/schema.pg.js +10 -45
  86. package/dist/identity/drizzle/schema.pg.js.map +1 -1
  87. package/dist/identity/drizzle/schema.sqlite.d.ts +88 -531
  88. package/dist/identity/drizzle/schema.sqlite.js +13 -46
  89. package/dist/identity/drizzle/schema.sqlite.js.map +1 -1
  90. package/dist/identity/oidc/ScopedPickWebIdHandler.d.ts +3 -0
  91. package/dist/identity/oidc/ScopedPickWebIdHandler.js +18 -6
  92. package/dist/identity/oidc/ScopedPickWebIdHandler.js.map +1 -1
  93. package/dist/identity/oidc/ScopedPickWebIdHandler.jsonld +22 -0
  94. package/dist/provision/ProvisionCodeCodec.js +10 -1
  95. package/dist/provision/ProvisionCodeCodec.js.map +1 -1
  96. package/dist/provision/ProvisionPodCreator.d.ts +8 -2
  97. package/dist/provision/ProvisionPodCreator.js +134 -41
  98. package/dist/provision/ProvisionPodCreator.js.map +1 -1
  99. package/dist/provision/ProvisionPodCreator.jsonld +38 -3
  100. package/dist/quota/DrizzleQuotaService.d.ts +0 -4
  101. package/dist/quota/DrizzleQuotaService.js +1 -21
  102. package/dist/quota/DrizzleQuotaService.js.map +1 -1
  103. package/dist/quota/DrizzleQuotaService.jsonld +0 -16
  104. package/dist/quota/NoopQuotaService.d.ts +0 -4
  105. package/dist/quota/NoopQuotaService.js +0 -8
  106. package/dist/quota/NoopQuotaService.js.map +1 -1
  107. package/dist/quota/NoopQuotaService.jsonld +0 -16
  108. package/dist/quota/QuotaService.d.ts +0 -4
  109. package/dist/quota/QuotaService.js.map +1 -1
  110. package/dist/quota/QuotaService.jsonld +0 -16
  111. package/dist/service/EdgeNodeSignalClient.d.ts +0 -2
  112. package/dist/service/EdgeNodeSignalClient.js +0 -4
  113. package/dist/service/EdgeNodeSignalClient.js.map +1 -1
  114. package/dist/service/PodMigrationService.d.ts +2 -2
  115. package/dist/service/PodMigrationService.js +4 -4
  116. package/dist/service/PodMigrationService.js.map +1 -1
  117. package/dist/setup/LocalSetupServiceTokenRepository.d.ts +22 -0
  118. package/dist/setup/LocalSetupServiceTokenRepository.js +68 -0
  119. package/dist/setup/LocalSetupServiceTokenRepository.js.map +1 -0
  120. package/dist/storage/quota/PerAccountQuotaStrategy.js +2 -2
  121. package/dist/storage/quota/PerAccountQuotaStrategy.js.map +1 -1
  122. package/dist/storage/quota/UsageRepository.d.ts +10 -32
  123. package/dist/storage/quota/UsageRepository.js +84 -281
  124. package/dist/storage/quota/UsageRepository.js.map +1 -1
  125. package/dist/subdomain/SubdomainService.d.ts +1 -1
  126. package/dist/subdomain/SubdomainService.js +1 -1
  127. package/dist/subdomain/SubdomainService.js.map +1 -1
  128. package/dist/subdomain/SubdomainService.jsonld +1 -1
  129. package/package.json +1 -1
  130. package/dist/api/handlers/ApiKeyHandler.d.ts +0 -15
  131. package/dist/api/handlers/ApiKeyHandler.js +0 -153
  132. package/dist/api/handlers/ApiKeyHandler.js.map +0 -1
  133. package/dist/api/store/DrizzleClientCredentialsStore.d.ts +0 -51
  134. package/dist/api/store/DrizzleClientCredentialsStore.js +0 -115
  135. package/dist/api/store/DrizzleClientCredentialsStore.js.map +0 -1
@@ -13,13 +13,41 @@
13
13
  * GET /provision/status - Local 端 SP 状态查询(公开)
14
14
  * 返回 SP 配置状态,供 Linx 查询
15
15
  */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
16
39
  Object.defineProperty(exports, "__esModule", { value: true });
17
40
  exports.registerProvisionRoutes = registerProvisionRoutes;
18
41
  exports.registerProvisionStatusRoute = registerProvisionStatusRoute;
42
+ exports.createLocalSetupProvisionStateWriter = createLocalSetupProvisionStateWriter;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const node_crypto_1 = require("node:crypto");
19
46
  const global_logger_factory_1 = require("global-logger-factory");
20
47
  const ProvisionCodeCodec_1 = require("../../provision/ProvisionCodeCodec");
21
48
  /** 默认 24 小时 */
22
49
  const DEFAULT_TTL = 24 * 60 * 60;
50
+ const PROVISION_STATUS_REFRESH_GRACE_SECONDS = 5 * 60;
23
51
  function registerProvisionRoutes(server, options) {
24
52
  const logger = (0, global_logger_factory_1.getLoggerFor)('ProvisionHandler');
25
53
  const { repository, baseUrl, baseStorageDomain } = options;
@@ -32,7 +60,7 @@ function registerProvisionRoutes(server, options) {
32
60
  *
33
61
  * Request:
34
62
  * {
35
- * publicUrl: string,
63
+ * publicUrl?: string,
36
64
  * nodeId?: string,
37
65
  * displayName?: string,
38
66
  * ipv4?: string,
@@ -55,41 +83,70 @@ function registerProvisionRoutes(server, options) {
55
83
  sendJson(response, 400, { error: 'Invalid JSON body' });
56
84
  return;
57
85
  }
58
- if (!body.publicUrl) {
59
- sendJson(response, 400, { error: 'publicUrl is required' });
60
- return;
61
- }
62
- try {
63
- new URL(body.publicUrl);
64
- }
65
- catch {
66
- sendJson(response, 400, { error: 'Invalid publicUrl format' });
67
- return;
68
- }
69
86
  try {
87
+ const domainMode = body.domainMode === 'self-managed' ? 'self-managed' : 'managed';
88
+ const requestedManagedDomain = normalizeRequestedManagedDomain(body.spDomain, baseStorageDomain);
89
+ const shouldAllocateManagedPublicUrl = !body.publicUrl && domainMode === 'managed' && Boolean(baseStorageDomain);
90
+ const preallocatedNodeId = shouldAllocateManagedPublicUrl
91
+ ? (body.nodeId ?? (0, node_crypto_1.randomUUID)())
92
+ : undefined;
93
+ const preallocatedSubdomainPrefix = preallocatedNodeId
94
+ ? resolveManagedSubdomainPrefix({
95
+ domainMode,
96
+ baseStorageDomain,
97
+ requestedManagedDomain,
98
+ nodeId: preallocatedNodeId,
99
+ })
100
+ : undefined;
101
+ const preallocatedSpDomain = preallocatedSubdomainPrefix
102
+ ? `${preallocatedSubdomainPrefix}.${baseStorageDomain}`
103
+ : undefined;
104
+ const effectivePublicUrl = body.publicUrl ?? derivePublicUrlFromSpDomain(preallocatedSpDomain);
105
+ if (!effectivePublicUrl) {
106
+ sendJson(response, 400, { error: 'publicUrl is required' });
107
+ return;
108
+ }
109
+ if (preallocatedSubdomainPrefix && baseStorageDomain && options.ddnsRepo) {
110
+ const existing = await options.ddnsRepo.getRecord(preallocatedSubdomainPrefix);
111
+ if (existing?.nodeId && existing.nodeId !== preallocatedNodeId) {
112
+ sendJson(response, 409, {
113
+ error: 'spDomain already allocated',
114
+ spDomain: preallocatedSpDomain,
115
+ });
116
+ return;
117
+ }
118
+ }
119
+ try {
120
+ new URL(effectivePublicUrl);
121
+ }
122
+ catch {
123
+ sendJson(response, 400, { error: 'Invalid publicUrl format' });
124
+ return;
125
+ }
70
126
  const result = await repository.registerSpNode({
71
- publicUrl: body.publicUrl,
127
+ publicUrl: effectivePublicUrl,
72
128
  displayName: body.displayName,
73
- nodeId: body.nodeId,
129
+ nodeId: preallocatedNodeId ?? body.nodeId,
74
130
  nodeToken: body.nodeToken,
75
131
  serviceToken: body.serviceToken,
76
132
  });
77
- const domainMode = body.domainMode === 'self-managed' ? 'self-managed' : 'managed';
78
- const requestedManagedDomain = normalizeRequestedManagedDomain(body.spDomain, baseStorageDomain);
79
- const subdomainPrefix = resolveManagedSubdomainPrefix({
80
- domainMode,
81
- baseStorageDomain,
82
- requestedManagedDomain,
83
- nodeId: result.nodeId,
84
- });
133
+ const subdomainPrefix = preallocatedSubdomainPrefix
134
+ ?? resolveManagedSubdomainPrefix({
135
+ domainMode,
136
+ baseStorageDomain,
137
+ requestedManagedDomain,
138
+ nodeId: result.nodeId,
139
+ });
85
140
  const spDomain = subdomainPrefix
86
141
  ? `${subdomainPrefix}.${baseStorageDomain}`
87
142
  : undefined;
143
+ const managedPublicUrl = derivePublicUrlFromSpDomain(spDomain);
144
+ const provisionSpUrl = body.publicUrl ?? managedPublicUrl ?? effectivePublicUrl;
88
145
  const tunnelState = await ensureManagedTunnelState({
89
146
  repository,
90
147
  nodeId: result.nodeId,
91
148
  subdomainPrefix,
92
- publicUrl: body.publicUrl,
149
+ publicUrl: provisionSpUrl,
93
150
  localPort: body.localPort,
94
151
  ipv4: body.ipv4,
95
152
  tunnelToken: body.tunnelToken,
@@ -107,19 +164,22 @@ function registerProvisionRoutes(server, options) {
107
164
  }
108
165
  // 生成自包含 provisionCode(编码了 SP 信息,CSS 解码后直接回调 SP)
109
166
  const provisionCode = codec.encode({
110
- spUrl: body.publicUrl,
167
+ spUrl: provisionSpUrl,
111
168
  serviceToken: result.serviceToken,
112
169
  nodeId: result.nodeId,
113
170
  spDomain,
114
171
  exp: Math.floor(Date.now() / 1000) + ttl,
115
172
  });
116
- logger.info(`Registered SP node ${result.nodeId} at ${body.publicUrl}${spDomain ? `, spDomain: ${spDomain}` : ''}`);
173
+ logger.info(`Registered SP node ${result.nodeId} at ${provisionSpUrl}${spDomain ? `, spDomain: ${spDomain}` : ''}`);
117
174
  const responseBody = {
118
175
  nodeId: result.nodeId,
119
176
  nodeToken: result.nodeToken,
120
177
  serviceToken: result.serviceToken,
121
178
  provisionCode,
122
179
  };
180
+ if (managedPublicUrl) {
181
+ responseBody.publicUrl = managedPublicUrl;
182
+ }
123
183
  if (spDomain) {
124
184
  responseBody.spDomain = spDomain;
125
185
  }
@@ -222,7 +282,6 @@ async function ensureManagedTunnelState(options) {
222
282
  localPort,
223
283
  configuredAt: new Date().toISOString(),
224
284
  },
225
- publicAddress: tunnelConfig.endpoint || publicUrl,
226
285
  });
227
286
  return {
228
287
  mode,
@@ -285,7 +344,6 @@ async function ensureManagedTokenTunnelState(options) {
285
344
  configuredAt: new Date().toISOString(),
286
345
  source: 'client-token',
287
346
  },
288
- publicAddress: endpoint || publicUrl,
289
347
  });
290
348
  return config;
291
349
  }
@@ -344,20 +402,79 @@ function readManagedTunnelConfig(metadata) {
344
402
  }
345
403
  function registerProvisionStatusRoute(server, options) {
346
404
  const logger = (0, global_logger_factory_1.getLoggerFor)('ProvisionStatusHandler');
405
+ const fetchImpl = options.fetchImpl ?? fetch;
406
+ const now = options.now ?? (() => Date.now());
407
+ const refreshGraceSeconds = options.refreshGraceSeconds ?? PROVISION_STATUS_REFRESH_GRACE_SECONDS;
408
+ const state = {
409
+ provisionCode: options.provisionCode,
410
+ nodeId: options.nodeId,
411
+ nodeToken: options.nodeToken,
412
+ serviceToken: options.serviceToken,
413
+ publicUrl: normalizeUrl(options.publicUrl),
414
+ spDomain: options.spDomain,
415
+ };
416
+ let refreshPromise;
347
417
  server.get('/provision/status', async (_request, response) => {
348
418
  const registered = Boolean(options.nodeId && options.cloudUrl);
349
419
  const body = {
350
420
  registered,
351
421
  };
352
422
  if (registered) {
423
+ const canRefresh = canRefreshProvisionStatus(options, state);
424
+ const currentNow = now();
425
+ const fresh = isProvisionCodeFresh(state.provisionCode, currentNow, refreshGraceSeconds);
426
+ const codeState = inspectProvisionCodeExpiration(state.provisionCode);
427
+ if (!canRefresh && codeState.kind !== 'missing' && !isProvisionCodeUsable(state.provisionCode, currentNow)) {
428
+ sendJson(response, 503, {
429
+ registered: true,
430
+ error: 'provision_refresh_unavailable',
431
+ message: 'Local provision state is expired and cannot be refreshed. Please restart Local or try again.',
432
+ });
433
+ return;
434
+ }
435
+ if (canRefresh && !fresh) {
436
+ let didRefresh = false;
437
+ refreshPromise ??= refreshProvisionStatus({
438
+ options,
439
+ state,
440
+ fetchImpl,
441
+ logger,
442
+ }).finally(() => {
443
+ refreshPromise = undefined;
444
+ });
445
+ try {
446
+ await refreshPromise;
447
+ didRefresh = true;
448
+ }
449
+ catch (error) {
450
+ logger.warn(`Failed to refresh provisionCode for ${state.nodeId}: ${error}`);
451
+ if (!isProvisionCodeUsable(state.provisionCode, now())) {
452
+ sendJson(response, 503, {
453
+ registered: true,
454
+ error: 'provision_refresh_failed',
455
+ message: 'Local provision state could not be refreshed. Please restart Local or try again.',
456
+ });
457
+ return;
458
+ }
459
+ }
460
+ if (didRefresh) {
461
+ await persistProvisionStatusState(options, state, logger);
462
+ }
463
+ }
353
464
  body.cloudUrl = options.cloudUrl;
354
- body.nodeId = options.nodeId;
355
- if (options.spDomain) {
356
- body.spDomain = options.spDomain;
465
+ body.nodeId = state.nodeId ?? options.nodeId;
466
+ if (state.spDomain) {
467
+ body.spDomain = state.spDomain;
468
+ }
469
+ if (state.publicUrl) {
470
+ body.publicUrl = state.publicUrl;
471
+ }
472
+ if (state.provisionCode) {
473
+ body.provisionCode = state.provisionCode;
357
474
  }
358
475
  if (options.cloudBaseUrl) {
359
- const provisionUrl = options.provisionCode
360
- ? `${options.cloudBaseUrl.replace(/\/$/, '')}/.account/?provisionCode=${encodeURIComponent(options.provisionCode)}`
476
+ const provisionUrl = state.provisionCode
477
+ ? `${options.cloudBaseUrl.replace(/\/$/, '')}/.account/?provisionCode=${encodeURIComponent(state.provisionCode)}`
361
478
  : `${options.cloudBaseUrl.replace(/\/$/, '')}/.account/`;
362
479
  body.provisionUrl = provisionUrl;
363
480
  }
@@ -366,6 +483,193 @@ function registerProvisionStatusRoute(server, options) {
366
483
  }, { public: true });
367
484
  logger.info('Provision status route registered');
368
485
  }
486
+ function createLocalSetupProvisionStateWriter(setupPath, providerId) {
487
+ if (!setupPath?.trim() || !providerId?.trim()) {
488
+ return undefined;
489
+ }
490
+ const targetPath = path.resolve(setupPath);
491
+ const targetProviderId = providerId.trim();
492
+ return async (state) => {
493
+ upsertLocalSetupFile(targetPath, targetProviderId, state);
494
+ };
495
+ }
496
+ async function persistProvisionStatusState(options, state, logger) {
497
+ if (!options.persistState || !state.nodeId || !state.nodeToken || !state.serviceToken || !state.provisionCode) {
498
+ return;
499
+ }
500
+ try {
501
+ await options.persistState({
502
+ nodeId: state.nodeId,
503
+ nodeToken: state.nodeToken,
504
+ serviceToken: state.serviceToken,
505
+ provisionCode: state.provisionCode,
506
+ publicUrl: state.publicUrl,
507
+ spDomain: state.spDomain,
508
+ cloudUrl: options.cloudUrl,
509
+ cloudBaseUrl: options.cloudBaseUrl,
510
+ });
511
+ }
512
+ catch (error) {
513
+ logger.warn(`Failed to persist refreshed local provision state: ${error}`);
514
+ }
515
+ }
516
+ function upsertLocalSetupFile(filePath, providerId, state) {
517
+ const existing = readJsonObjectFile(filePath);
518
+ const previous = readJsonObject(existing[providerId]);
519
+ const cloudIdentityUrl = normalizeUrl(state.cloudBaseUrl);
520
+ const cloudApiUrl = normalizeUrl(state.cloudUrl);
521
+ const provisionUrl = cloudIdentityUrl
522
+ ? `${cloudIdentityUrl.replace(/\/+$/u, '')}/.account/?provisionCode=${encodeURIComponent(state.provisionCode)}`
523
+ : readString(previous.provisionUrl);
524
+ existing[providerId] = {
525
+ ...previous,
526
+ nodeId: state.nodeId,
527
+ nodeToken: state.nodeToken,
528
+ serviceToken: state.serviceToken,
529
+ provisionCode: state.provisionCode,
530
+ publicUrl: normalizeUrl(state.publicUrl),
531
+ spDomain: readString(state.spDomain),
532
+ provisionUrl,
533
+ cloudIdentityUrl,
534
+ cloudApiUrl,
535
+ registeredAt: typeof previous.registeredAt === 'number' && Number.isFinite(previous.registeredAt)
536
+ ? previous.registeredAt
537
+ : Date.now(),
538
+ };
539
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
540
+ fs.writeFileSync(filePath, `${JSON.stringify(existing, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
541
+ try {
542
+ fs.chmodSync(filePath, 0o600);
543
+ }
544
+ catch {
545
+ // Some filesystems do not support chmod; the local runtime can still proceed.
546
+ }
547
+ }
548
+ function readJsonObjectFile(filePath) {
549
+ try {
550
+ if (!fs.existsSync(filePath)) {
551
+ return {};
552
+ }
553
+ return readJsonObject(JSON.parse(fs.readFileSync(filePath, 'utf8')));
554
+ }
555
+ catch {
556
+ return {};
557
+ }
558
+ }
559
+ function readJsonObject(value) {
560
+ return value && typeof value === 'object' && !Array.isArray(value)
561
+ ? value
562
+ : {};
563
+ }
564
+ function readString(value) {
565
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
566
+ }
567
+ function canRefreshProvisionStatus(options, state) {
568
+ return Boolean(options.cloudUrl
569
+ && state.nodeId
570
+ && state.nodeToken
571
+ && state.serviceToken
572
+ && state.publicUrl);
573
+ }
574
+ async function refreshProvisionStatus(options) {
575
+ const { options: statusOptions, state, fetchImpl, logger } = options;
576
+ const endpoint = new URL('/provision/nodes', ensureTrailingSlash(statusOptions.cloudUrl)).toString();
577
+ const requestBody = {
578
+ publicUrl: state.publicUrl,
579
+ nodeId: state.nodeId,
580
+ nodeToken: state.nodeToken,
581
+ serviceToken: state.serviceToken,
582
+ domainMode: state.spDomain ? 'managed' : 'self-managed',
583
+ spDomain: state.spDomain,
584
+ };
585
+ if (statusOptions.localPort && statusOptions.localPort > 0) {
586
+ requestBody.localPort = statusOptions.localPort;
587
+ }
588
+ if (statusOptions.tunnelToken) {
589
+ requestBody.tunnelToken = statusOptions.tunnelToken;
590
+ requestBody.tunnelMode = 'client';
591
+ }
592
+ const result = await fetchImpl(endpoint, {
593
+ method: 'POST',
594
+ headers: {
595
+ Accept: 'application/json',
596
+ 'Content-Type': 'application/json',
597
+ },
598
+ body: JSON.stringify(requestBody),
599
+ });
600
+ if (!result.ok) {
601
+ const detail = await result.text().catch(() => '');
602
+ throw new Error(detail || `HTTP ${result.status}`);
603
+ }
604
+ const payload = await result.json().catch(() => undefined);
605
+ if (!payload
606
+ || typeof payload.nodeId !== 'string'
607
+ || typeof payload.nodeToken !== 'string'
608
+ || typeof payload.serviceToken !== 'string'
609
+ || typeof payload.provisionCode !== 'string') {
610
+ throw new Error('Cloud returned an incomplete provision refresh response.');
611
+ }
612
+ state.nodeId = payload.nodeId;
613
+ state.nodeToken = payload.nodeToken;
614
+ state.serviceToken = payload.serviceToken;
615
+ state.provisionCode = payload.provisionCode;
616
+ state.publicUrl = normalizeUrl(payload.publicUrl) ?? state.publicUrl;
617
+ state.spDomain = typeof payload.spDomain === 'string' ? payload.spDomain : state.spDomain;
618
+ process.env.XPOD_NODE_ID = state.nodeId;
619
+ process.env.XPOD_NODE_TOKEN = state.nodeToken;
620
+ process.env.XPOD_SERVICE_TOKEN = state.serviceToken;
621
+ process.env.XPOD_PROVISION_CODE = state.provisionCode;
622
+ if (statusOptions.cloudBaseUrl) {
623
+ process.env.XPOD_PROVISION_URL = `${statusOptions.cloudBaseUrl.replace(/\/$/u, '')}/.account/?provisionCode=${encodeURIComponent(state.provisionCode)}`;
624
+ }
625
+ if (state.spDomain) {
626
+ process.env.XPOD_SP_DOMAIN = state.spDomain;
627
+ }
628
+ logger.info(`Refreshed provisionCode for ${state.nodeId}`);
629
+ }
630
+ function isProvisionCodeFresh(code, nowMs, graceSeconds) {
631
+ const state = inspectProvisionCodeExpiration(code);
632
+ return state.kind === 'self-contained' && state.expiresAt > Math.floor(nowMs / 1000) + graceSeconds;
633
+ }
634
+ function isProvisionCodeUsable(code, nowMs) {
635
+ const state = inspectProvisionCodeExpiration(code);
636
+ if (state.kind === 'legacy') {
637
+ return true;
638
+ }
639
+ return state.kind === 'self-contained' && state.expiresAt > Math.floor(nowMs / 1000);
640
+ }
641
+ function inspectProvisionCodeExpiration(code) {
642
+ if (!code) {
643
+ return { kind: 'missing' };
644
+ }
645
+ const dotIndex = code.indexOf('.');
646
+ if (dotIndex <= 0) {
647
+ return { kind: 'legacy' };
648
+ }
649
+ try {
650
+ const payload = JSON.parse(Buffer.from(code.slice(0, dotIndex), 'base64url').toString('utf8'));
651
+ return typeof payload.exp === 'number' && Number.isFinite(payload.exp)
652
+ ? { kind: 'self-contained', expiresAt: payload.exp }
653
+ : { kind: 'invalid' };
654
+ }
655
+ catch {
656
+ return { kind: 'invalid' };
657
+ }
658
+ }
659
+ function ensureTrailingSlash(value) {
660
+ return value.endsWith('/') ? value : `${value}/`;
661
+ }
662
+ function normalizeUrl(value) {
663
+ if (!value?.trim()) {
664
+ return undefined;
665
+ }
666
+ try {
667
+ return new URL(value.trim()).toString().replace(/\/+$/u, '') + '/';
668
+ }
669
+ catch {
670
+ return value.trim().replace(/\/+$/u, '') + '/';
671
+ }
672
+ }
369
673
  async function readJsonBody(request) {
370
674
  return new Promise((resolve, reject) => {
371
675
  let data = '';
@@ -393,6 +697,9 @@ function sendJson(response, status, data) {
393
697
  response.setHeader('Content-Type', 'application/json');
394
698
  response.end(JSON.stringify(data));
395
699
  }
700
+ function derivePublicUrlFromSpDomain(spDomain) {
701
+ return spDomain ? `https://${spDomain}/` : undefined;
702
+ }
396
703
  function normalizeRequestedManagedDomain(value, baseStorageDomain) {
397
704
  if (!value || !baseStorageDomain) {
398
705
  return undefined;