@wtfalch/foundry 0.17.1 → 0.18.0
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.
- package/dist/bootstrap/declare.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0045_names_to_edge.sql +107 -0
- package/dist/org.d.ts +17 -17
- package/dist/org.js +48 -58
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +16 -16
- package/dist/plan/entry-edge-key.d.ts +79 -0
- package/dist/plan/{entry-names-key.js → entry-edge-key.js} +90 -66
- package/dist/plan/executor.d.ts +48 -37
- package/dist/plan/executor.js +166 -98
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/plan-apply.d.ts +6 -5
- package/dist/plan/plan-apply.js +40 -38
- package/dist/plan/platform-diff.d.ts +22 -22
- package/dist/plan/platform-diff.js +45 -45
- package/dist/plan/platform-org.d.ts +8 -8
- package/dist/plan/platform-org.js +7 -7
- package/dist/plan/platform-providers.d.ts +14 -14
- package/dist/plan/platform-providers.js +107 -107
- package/dist/plan/step-system.js +1 -1
- package/dist/plan/{edge-worker.d.ts → worker.d.ts} +53 -33
- package/dist/plan/{edge-worker.js → worker.js} +92 -54
- package/dist/platform.d.ts +3 -3
- package/dist/platform.js +1 -1
- package/dist/store/postgres.js +7 -7
- package/dist/store/schema.d.ts +6 -6
- package/dist/store/schema.js +4 -4
- package/package.json +1 -1
- package/dist/plan/entry-names-key.d.ts +0 -73
|
@@ -23,11 +23,11 @@ export const DEFAULT_GIT_REF = 'main';
|
|
|
23
23
|
*/
|
|
24
24
|
export const DEFAULT_MAIL_IMAGE_TAG = VERSIONS.stalwart;
|
|
25
25
|
/**
|
|
26
|
-
* Every platform service except `sfu` and `
|
|
26
|
+
* Every platform service except `sfu` and `edge` is enabled, like the other
|
|
27
27
|
* steps: a service whose dependencies the host did not supply is reported
|
|
28
28
|
* blocked, not planned. `sfu` stays off because it needs a held SSH key and
|
|
29
|
-
* host fingerprint, a separate human step (issue 170). `
|
|
30
|
-
* because the
|
|
29
|
+
* host fingerprint, a separate human step (issue 170). `edge` stays off
|
|
30
|
+
* because the edge service (N1) does not exist yet -- a human deploys it
|
|
31
31
|
* and sets its hostname before enabling it (#317's own "Human steps").
|
|
32
32
|
*/
|
|
33
33
|
const SERVICES_ENABLED_BY_DEFAULT = new Set([
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { FoundryStoreDb } from './store/postgres.js';
|
|
|
20
20
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
21
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, PlatformPlanDetail, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan/index.js';
|
|
22
22
|
export { AdoptRefusedError } from './plan/index.js';
|
|
23
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY,
|
|
23
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
24
24
|
export type { HeldKeyResolver, OrgPlatformReadiness } from './plan/index.js';
|
|
25
25
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
26
26
|
export type { PlatformOrgClientFactories } from './plan/index.js';
|
|
@@ -35,7 +35,7 @@ export { checkMailIdentityAudience } from './plan/index.js';
|
|
|
35
35
|
export type { MailAllowlistApp } from './plan/index.js';
|
|
36
36
|
export { checkMailAppsAllowlist } from './plan/index.js';
|
|
37
37
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
|
|
38
|
-
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime,
|
|
38
|
+
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, PlatformEdgeDeps, PlatformEdgeRuntime, } from './plan/index.js';
|
|
39
39
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './plan/index.js';
|
|
40
40
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './plan/index.js';
|
|
41
41
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, cre
|
|
|
12
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
13
13
|
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, } from './store/schema.js';
|
|
14
14
|
export { AdoptRefusedError } from './plan/index.js';
|
|
15
|
-
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY,
|
|
15
|
+
export { readLiveResources, readLivePlatformResources, computePlanDetail, toPlan, planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan/index.js';
|
|
16
16
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './plan/index.js';
|
|
17
17
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
|
|
18
18
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the names service is renamed edge, and the hosting entry
|
|
2
|
+
-- field `edge` (a Cloudflare Worker) is renamed `worker`. No behaviour change.
|
|
3
|
+
-- Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` columns.
|
|
4
|
+
--
|
|
5
|
+
-- 1. foundry_declared_platform: names_enabled / names_hostname /
|
|
6
|
+
-- names_git_ref become edge_*. Each rename runs only while the old column
|
|
7
|
+
-- exists, so a second run is a no-op.
|
|
8
|
+
-- 2. The hostname moves from `names.<apex>` to `edge.<apex>` only where it
|
|
9
|
+
-- still equals the old default (0043); an operator's own value stays.
|
|
10
|
+
-- 3. Recorded ids are rewritten in place: `names:key:`, `names:reserved-labels:`
|
|
11
|
+
-- and `platform:names` (ids and step ids), and the colon-free scope keys
|
|
12
|
+
-- `names_*` and `*_platform_names*`. The applied-hostname value is left
|
|
13
|
+
-- alone, so the planner sees a changed hostname and applies the step again.
|
|
14
|
+
-- Every WHERE clause matches only text that still holds an old key, so a
|
|
15
|
+
-- second run changes nothing. `foundry_audit_events` is a signed chain and
|
|
16
|
+
-- is not touched.
|
|
17
|
+
-- 4. foundry_declared_orgs.hosting_entries: each entry's `uses.names` becomes
|
|
18
|
+
-- `uses.edge`, and its `edge` becomes `worker`.
|
|
19
|
+
|
|
20
|
+
do $$
|
|
21
|
+
begin
|
|
22
|
+
if exists (select 1 from information_schema.columns
|
|
23
|
+
where table_schema = current_schema()
|
|
24
|
+
and table_name = 'foundry_declared_platform' and column_name = 'names_enabled') then
|
|
25
|
+
alter table foundry_declared_platform rename column names_enabled to edge_enabled;
|
|
26
|
+
end if;
|
|
27
|
+
if exists (select 1 from information_schema.columns
|
|
28
|
+
where table_schema = current_schema()
|
|
29
|
+
and table_name = 'foundry_declared_platform' and column_name = 'names_hostname') then
|
|
30
|
+
alter table foundry_declared_platform rename column names_hostname to edge_hostname;
|
|
31
|
+
end if;
|
|
32
|
+
if exists (select 1 from information_schema.columns
|
|
33
|
+
where table_schema = current_schema()
|
|
34
|
+
and table_name = 'foundry_declared_platform' and column_name = 'names_git_ref') then
|
|
35
|
+
alter table foundry_declared_platform rename column names_git_ref to edge_git_ref;
|
|
36
|
+
end if;
|
|
37
|
+
end
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
update foundry_declared_platform
|
|
41
|
+
set edge_hostname = 'edge.' || apex
|
|
42
|
+
where edge_hostname = 'names.' || apex;
|
|
43
|
+
|
|
44
|
+
-- The old-to-new text rewrite for recorded ids, applied to the jsonb as text.
|
|
45
|
+
create or replace function pg_temp.names_to_edge(value jsonb) returns jsonb
|
|
46
|
+
language sql immutable as $fn$
|
|
47
|
+
select replace(
|
|
48
|
+
replace(
|
|
49
|
+
replace(
|
|
50
|
+
replace(
|
|
51
|
+
replace(value::text, '"names:key:', '"edge:key:'),
|
|
52
|
+
'"names:reserved-labels:', '"edge:reserved-labels:'),
|
|
53
|
+
'"names_', '"edge_'),
|
|
54
|
+
'_platform_names', '_platform_edge'),
|
|
55
|
+
'platform:names', 'platform:edge')::jsonb
|
|
56
|
+
$fn$;
|
|
57
|
+
|
|
58
|
+
update foundry_declared_platform
|
|
59
|
+
set service_ids = pg_temp.names_to_edge(service_ids)
|
|
60
|
+
where service_ids::text ~ '"names[:_]|_platform_names|platform:names';
|
|
61
|
+
|
|
62
|
+
update foundry_applied_state
|
|
63
|
+
set service_ids = pg_temp.names_to_edge(service_ids)
|
|
64
|
+
where service_ids::text ~ '"names[:_]|_platform_names|platform:names';
|
|
65
|
+
|
|
66
|
+
update foundry_applied_state
|
|
67
|
+
set checkpoints = pg_temp.names_to_edge(checkpoints)
|
|
68
|
+
where checkpoints::text ~ '"names[:_]|_platform_names|platform:names';
|
|
69
|
+
|
|
70
|
+
update foundry_applied_state
|
|
71
|
+
set destroy = pg_temp.names_to_edge(destroy)
|
|
72
|
+
where destroy is not null
|
|
73
|
+
and destroy::text ~ '"names[:_]|_platform_names|platform:names';
|
|
74
|
+
|
|
75
|
+
update foundry_runs
|
|
76
|
+
set step_ids = pg_temp.names_to_edge(step_ids)
|
|
77
|
+
where step_ids::text ~ '"names[:_]|_platform_names|platform:names';
|
|
78
|
+
|
|
79
|
+
update foundry_runs
|
|
80
|
+
set checkpoints = pg_temp.names_to_edge(checkpoints)
|
|
81
|
+
where checkpoints::text ~ '"names[:_]|_platform_names|platform:names';
|
|
82
|
+
|
|
83
|
+
update foundry_runs
|
|
84
|
+
set failure = pg_temp.names_to_edge(failure)
|
|
85
|
+
where failure is not null
|
|
86
|
+
and failure::text ~ '"names[:_]|_platform_names|platform:names';
|
|
87
|
+
|
|
88
|
+
update foundry_declared_orgs o
|
|
89
|
+
set hosting_entries = (
|
|
90
|
+
select coalesce(jsonb_agg(
|
|
91
|
+
case
|
|
92
|
+
when jsonb_typeof(s -> 'uses') = 'object' and (s -> 'uses') ? 'names'
|
|
93
|
+
then jsonb_set(s, '{uses}',
|
|
94
|
+
((s -> 'uses') - 'names') || jsonb_build_object('edge', s -> 'uses' -> 'names'))
|
|
95
|
+
else s
|
|
96
|
+
end order by t.ord), '[]'::jsonb)
|
|
97
|
+
from jsonb_array_elements(o.hosting_entries) with ordinality as t(e, ord),
|
|
98
|
+
lateral (select case
|
|
99
|
+
when jsonb_typeof(t.e) = 'object' and t.e ? 'edge'
|
|
100
|
+
then (t.e - 'edge') || jsonb_build_object('worker', t.e -> 'edge')
|
|
101
|
+
else t.e
|
|
102
|
+
end as s) as r(s))
|
|
103
|
+
where exists (
|
|
104
|
+
select 1 from jsonb_array_elements(o.hosting_entries) e
|
|
105
|
+
where jsonb_typeof(e) = 'object'
|
|
106
|
+
and (e ? 'edge' or (jsonb_typeof(e -> 'uses') = 'object' and (e -> 'uses') ? 'names'))
|
|
107
|
+
);
|
package/dist/org.d.ts
CHANGED
|
@@ -335,26 +335,26 @@ export interface HostingEntry {
|
|
|
335
335
|
* F8: a Cloudflare Worker foundry deploys from this entry's own repo,
|
|
336
336
|
* beside the app, on the org's own zone. Only our own repos may use this
|
|
337
337
|
* (#318: outside customers need Workers for Platforms, not built yet), so
|
|
338
|
-
* `
|
|
338
|
+
* `validateEntryWorker` refuses it unless `repo` is set (every non-null
|
|
339
339
|
* `repo` already matches `wtfalch/...`, {@link REPO}) and the route's zone
|
|
340
340
|
* is the org's own domain.
|
|
341
341
|
*/
|
|
342
|
-
|
|
342
|
+
worker?: WorkerConfig;
|
|
343
343
|
}
|
|
344
344
|
/**
|
|
345
345
|
* One Cloudflare rate limiting binding on a Worker (#318) -- Cloudflare's own
|
|
346
346
|
* "simple" shape: a fixed number of requests per period, where `period` is
|
|
347
347
|
* the only two values Cloudflare's rate limiting binding accepts.
|
|
348
348
|
*/
|
|
349
|
-
export interface
|
|
349
|
+
export interface WorkerRateLimitBinding {
|
|
350
350
|
name: string;
|
|
351
351
|
namespaceId: string;
|
|
352
352
|
limit: number;
|
|
353
353
|
period: 10 | 60;
|
|
354
354
|
}
|
|
355
|
-
/** {@link HostingEntry.
|
|
356
|
-
export interface
|
|
357
|
-
rateLimits:
|
|
355
|
+
/** {@link HostingEntry.worker}'s bindings, one list per binding kind -- only rate limits today. */
|
|
356
|
+
export interface WorkerBindings {
|
|
357
|
+
rateLimits: WorkerRateLimitBinding[];
|
|
358
358
|
}
|
|
359
359
|
/**
|
|
360
360
|
* A plain Worker deployed beside a hosting entry (#318), named by one route
|
|
@@ -365,14 +365,14 @@ export interface EdgeBindings {
|
|
|
365
365
|
* Worker setup": "The app's automated build process builds the bundle
|
|
366
366
|
* outside Foundry; Foundry consumes and deploys the artifact through the
|
|
367
367
|
* provider boundary... No manual build step and no source checkout/bundling
|
|
368
|
-
* in Foundry"). `
|
|
368
|
+
* in Foundry"). `validateEntryWorker` refuses anything that is not an
|
|
369
369
|
* `https://` URL -- a bare path or relative file name is exactly the shape
|
|
370
370
|
* this decision rules out.
|
|
371
371
|
*/
|
|
372
|
-
export interface
|
|
372
|
+
export interface WorkerConfig {
|
|
373
373
|
/** Where foundry fetches the built module from, behind `WorkersPort` -- never checked out or bundled here. */
|
|
374
374
|
artifactUrl: string;
|
|
375
|
-
/** The built module's sha256, lowercase hex. Verified against the fetched bytes before upload, and the sole signal `
|
|
375
|
+
/** The built module's sha256, lowercase hex. Verified against the fetched bytes before upload, and the sole signal `planWorkers` diffs on: an org that rebuilds under an unchanged URL redeploys only when this changes. */
|
|
376
376
|
artifactSha256: string;
|
|
377
377
|
/**
|
|
378
378
|
* One route per entry today. A wildcard route (`*.<domain>/*`) matches
|
|
@@ -386,7 +386,7 @@ export interface EdgeConfig {
|
|
|
386
386
|
* separate entry with that exact route; there is no exclusion list.
|
|
387
387
|
*/
|
|
388
388
|
route: string;
|
|
389
|
-
bindings?:
|
|
389
|
+
bindings?: WorkerBindings;
|
|
390
390
|
}
|
|
391
391
|
/**
|
|
392
392
|
* Sign-in for one entry: one ZITADEL client and one broker entry, signing in
|
|
@@ -441,16 +441,16 @@ export interface HostingEntryUses {
|
|
|
441
441
|
ai?: EntryAi;
|
|
442
442
|
files?: EntryFiles;
|
|
443
443
|
/**
|
|
444
|
-
* F7 (#317): the entry gets its own
|
|
445
|
-
* org with the
|
|
446
|
-
* Serves only the org's own domain (
|
|
447
|
-
* so `
|
|
444
|
+
* F7 (#317): the entry gets its own edge key, minted directly below the
|
|
445
|
+
* org with the edge service, and `EDGE_URL`/`EDGE_API_KEY` in its env.
|
|
446
|
+
* Serves only the org's own domain (edge claims `<label>.<org's domain>`),
|
|
447
|
+
* so `validateEntryEdge` refuses it without `hosting.usesDomain` and an
|
|
448
448
|
* org `domain`.
|
|
449
449
|
*/
|
|
450
|
-
|
|
450
|
+
edge?: true;
|
|
451
451
|
}
|
|
452
|
-
/** The env names `uses.
|
|
453
|
-
export declare const
|
|
452
|
+
/** The env names `uses.edge` writes; `env` may not name them (#317). */
|
|
453
|
+
export declare const EDGE_ENTRY_ENV_NAMES: readonly ["EDGE_URL", "EDGE_API_KEY"];
|
|
454
454
|
/**
|
|
455
455
|
* A plain string is pushed as it is, so `validateHosting` refuses one that
|
|
456
456
|
* looks like a secret. `{ from }` names a value the plan already mints.
|
package/dist/org.js
CHANGED
|
@@ -36,8 +36,8 @@ export const FILES_ENTRY_ENV_NAMES = [
|
|
|
36
36
|
'FILES_API_KEY',
|
|
37
37
|
];
|
|
38
38
|
export const filesCollectionEnvName = (name) => `FILES_COLLECTION_${name}_ID`;
|
|
39
|
-
/** The env names `uses.
|
|
40
|
-
export const
|
|
39
|
+
/** The env names `uses.edge` writes; `env` may not name them (#317). */
|
|
40
|
+
export const EDGE_ENTRY_ENV_NAMES = ['EDGE_URL', 'EDGE_API_KEY'];
|
|
41
41
|
/** The org's `app` entry: today's one app. */
|
|
42
42
|
export function appEntry(hosting) {
|
|
43
43
|
return hosting.entries.find((entry) => entry.key === APP_ENTRY_KEY);
|
|
@@ -132,29 +132,29 @@ export function validateHosting(hosting, names) {
|
|
|
132
132
|
const invalidFiles = validateEntryFiles(entry, hosting, names?.domain);
|
|
133
133
|
if (invalidFiles)
|
|
134
134
|
return invalidFiles;
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
135
|
+
const invalidWorker = validateEntryWorker(entry, hosting, names?.domain);
|
|
136
|
+
if (invalidWorker)
|
|
137
|
+
return invalidWorker;
|
|
138
|
+
const invalidEdgeUse = validateEntryEdge(entry, hosting, names?.domain);
|
|
139
|
+
if (invalidEdgeUse)
|
|
140
|
+
return invalidEdgeUse;
|
|
141
141
|
}
|
|
142
142
|
if (hosting.entries.filter((entry) => entry.uses?.ai?.webhookPath !== undefined).length > 1) {
|
|
143
143
|
return 'at most one hosting entry may set uses.ai webhookPath: ai keeps one webhook per organisation';
|
|
144
144
|
}
|
|
145
|
-
// #318 re-review: two entries declaring the same
|
|
145
|
+
// #318 re-review: two entries declaring the same worker route both throw at
|
|
146
146
|
// apply (`cloudflareWorker` refuses a route already bound to a Worker
|
|
147
147
|
// that is not its own), one of which stops Agora and everything planned
|
|
148
148
|
// after it provisioning at all -- refused here instead, before an apply
|
|
149
149
|
// ever reaches it.
|
|
150
|
-
const
|
|
150
|
+
const seenWorkerRoutes = new Set();
|
|
151
151
|
for (const entry of hosting.entries) {
|
|
152
|
-
if (!entry.
|
|
152
|
+
if (!entry.worker)
|
|
153
153
|
continue;
|
|
154
|
-
if (
|
|
155
|
-
return `hosting.entries '${entry.key}'
|
|
154
|
+
if (seenWorkerRoutes.has(entry.worker.route)) {
|
|
155
|
+
return `hosting.entries '${entry.key}' worker route '${entry.worker.route}' is already declared by another entry`;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
seenWorkerRoutes.add(entry.worker.route);
|
|
158
158
|
}
|
|
159
159
|
const invalidNames = validateEntryHostnames(hosting, names);
|
|
160
160
|
if (invalidNames)
|
|
@@ -438,27 +438,27 @@ function validateEntryFiles(entry, hosting, domain) {
|
|
|
438
438
|
return null;
|
|
439
439
|
}
|
|
440
440
|
/** A route pattern's zone apex: the host part before the first `/`, with one leading `*.` label stripped. */
|
|
441
|
-
function
|
|
441
|
+
function workerRouteZone(route) {
|
|
442
442
|
const slash = route.indexOf('/');
|
|
443
443
|
if (slash < 1)
|
|
444
444
|
return null;
|
|
445
445
|
const host = route.slice(0, slash);
|
|
446
446
|
return host.startsWith('*.') ? host.slice(2) : host;
|
|
447
447
|
}
|
|
448
|
-
const
|
|
449
|
-
const
|
|
448
|
+
const WORKER_ROUTE = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+\/.*$/;
|
|
449
|
+
const WORKER_SHA256 = /^[0-9a-f]{64}$/;
|
|
450
450
|
/**
|
|
451
|
-
* Restated, not shared, from `plan/
|
|
452
|
-
* `
|
|
451
|
+
* Restated, not shared, from `plan/worker.ts`'s `WORKER_ORG_HASH_LENGTH`/
|
|
452
|
+
* `WORKER_NAME_MAX_LENGTH` -- importing them here would import that
|
|
453
453
|
* module's own `DeclaredOrg`/`HostingEntry` imports right back into this
|
|
454
|
-
* one. `
|
|
454
|
+
* one. `workerName`'s shape is `${8-hex-char hash}-${entry.key}-edge`;
|
|
455
455
|
* the hash and the `-edge` suffix are fixed length, so the entry key's own
|
|
456
456
|
* budget against Cloudflare's 63-character Worker name limit is fixed too.
|
|
457
457
|
*/
|
|
458
|
-
const
|
|
459
|
-
const
|
|
458
|
+
const WORKER_NAME_FIXED_LENGTH = 8 + 1 + '-edge'.length;
|
|
459
|
+
const WORKER_NAME_MAX_LENGTH = 63;
|
|
460
460
|
/**
|
|
461
|
-
* `
|
|
461
|
+
* `worker`: the entry's own repo (never the shared standard app -- entries
|
|
462
462
|
* with `repo: null` have no Worker source of their own), an `https://`
|
|
463
463
|
* artifact URL with a lowercase-hex sha256 (never a bare path or relative
|
|
464
464
|
* file -- estate DECISIONS.md, 2026-10-04, rules out any checkout or
|
|
@@ -467,22 +467,22 @@ const EDGE_WORKER_NAME_MAX_LENGTH = 63;
|
|
|
467
467
|
* binding's `period` restricted to the two values Cloudflare's simple rate
|
|
468
468
|
* limiting binding accepts.
|
|
469
469
|
*/
|
|
470
|
-
function
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
470
|
+
function validateEntryWorker(entry, hosting, domain) {
|
|
471
|
+
const worker = entry.worker;
|
|
472
|
+
if (!worker)
|
|
473
473
|
return null;
|
|
474
|
-
const at = `hosting.entries '${entry.key}'
|
|
474
|
+
const at = `hosting.entries '${entry.key}' worker`;
|
|
475
475
|
if (!hosting.enabled)
|
|
476
476
|
return `${at} requires hosting.enabled`;
|
|
477
477
|
if (entry.repo === null) {
|
|
478
478
|
return `${at} needs its own repo (repo: null is the shared standard app, not a Worker source)`;
|
|
479
479
|
}
|
|
480
|
-
if (entry.key.length >
|
|
481
|
-
return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "-edge") must fit Cloudflare's ${
|
|
480
|
+
if (entry.key.length > WORKER_NAME_MAX_LENGTH - WORKER_NAME_FIXED_LENGTH) {
|
|
481
|
+
return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "-edge") must fit Cloudflare's ${WORKER_NAME_MAX_LENGTH}-character limit`;
|
|
482
482
|
}
|
|
483
483
|
let artifactUrl;
|
|
484
484
|
try {
|
|
485
|
-
artifactUrl = new URL(
|
|
485
|
+
artifactUrl = new URL(worker.artifactUrl);
|
|
486
486
|
}
|
|
487
487
|
catch {
|
|
488
488
|
return `${at} artifactUrl must be an absolute URL, not a bare path or relative file`;
|
|
@@ -497,32 +497,22 @@ function validateEntryEdge(entry, hosting, domain, locked) {
|
|
|
497
497
|
return `${at} artifactUrl must not carry a query string (a signed or credentialed URL ends up stored verbatim in recorded scope ids)`;
|
|
498
498
|
}
|
|
499
499
|
// `URL.hash` is '' for a bare trailing `#`, so the raw string is checked.
|
|
500
|
-
if (
|
|
500
|
+
if (worker.artifactUrl.includes('#')) {
|
|
501
501
|
return `${at} artifactUrl must not carry a fragment (refused for the same reason as a query string)`;
|
|
502
502
|
}
|
|
503
|
-
if (!
|
|
503
|
+
if (!WORKER_SHA256.test(worker.artifactSha256)) {
|
|
504
504
|
return `${at} artifactSha256 must be 64 lowercase hex characters`;
|
|
505
505
|
}
|
|
506
|
-
if (!
|
|
506
|
+
if (!WORKER_ROUTE.test(worker.route)) {
|
|
507
507
|
return `${at} route must be a hostname (an optional leading '*.' label, then at least two more labels) and a path, joined by '/'`;
|
|
508
508
|
}
|
|
509
|
-
const zone =
|
|
509
|
+
const zone = workerRouteZone(worker.route);
|
|
510
510
|
if (!domain || zone !== domain) {
|
|
511
|
-
return `${at} route '${
|
|
512
|
-
}
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
// `locked-hostnames.ts`'s set -- not only the org's other entries. (A
|
|
517
|
-
// non-wildcard route can only ever be the bare apex here -- the check
|
|
518
|
-
// just above requires `edgeRouteZone(edge.route) === domain`, and that
|
|
519
|
-
// function returns a non-wildcard route's host unchanged -- and the apex
|
|
520
|
-
// alone is never itself a locked name, so there is no direct-match case
|
|
521
|
-
// to refuse alongside this one.)
|
|
522
|
-
if (locked && locked.size > 0 && edge.route.startsWith('*.')) {
|
|
523
|
-
return `${at} route '${edge.route}' is a wildcard: it would also capture ${[...locked].sort().join(', ')}, every hostname foundry already owns under this domain`;
|
|
524
|
-
}
|
|
525
|
-
for (const limit of edge.bindings?.rateLimits ?? []) {
|
|
511
|
+
return `${at} route '${worker.route}' must be on the org's own domain${domain ? ` (${domain})` : ''}: worker is only for our own projects`;
|
|
512
|
+
}
|
|
513
|
+
// A wildcard route (`*.<domain>/*`) is allowed beside hostnames foundry owns:
|
|
514
|
+
// `planWorkers` plans a no-Worker route for each of them, so they stay out of it.
|
|
515
|
+
for (const limit of worker.bindings?.rateLimits ?? []) {
|
|
526
516
|
if (!limit.name.trim())
|
|
527
517
|
return `${at} bindings.rateLimits needs a non-empty name`;
|
|
528
518
|
if (!limit.namespaceId.trim())
|
|
@@ -537,23 +527,23 @@ function validateEntryEdge(entry, hosting, domain, locked) {
|
|
|
537
527
|
return null;
|
|
538
528
|
}
|
|
539
529
|
/**
|
|
540
|
-
* `uses.
|
|
530
|
+
* `uses.edge`: serves only the org's own domain (edge claims
|
|
541
531
|
* `<label>.<domain>`), so it needs `hosting.usesDomain` and a `domain`, and
|
|
542
|
-
* no `env` entry that names a value `uses.
|
|
532
|
+
* no `env` entry that names a value `uses.edge` writes.
|
|
543
533
|
*/
|
|
544
|
-
function
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
534
|
+
function validateEntryEdge(entry, hosting, domain) {
|
|
535
|
+
const edge = entry.uses?.edge;
|
|
536
|
+
if (edge === undefined)
|
|
547
537
|
return null;
|
|
548
|
-
const at = `hosting.entries '${entry.key}' uses.
|
|
549
|
-
if (
|
|
538
|
+
const at = `hosting.entries '${entry.key}' uses.edge`;
|
|
539
|
+
if (edge !== true)
|
|
550
540
|
return `${at} must be true`;
|
|
551
541
|
if (!hosting.enabled)
|
|
552
542
|
return `${at} requires hosting.enabled`;
|
|
553
543
|
if (!hosting.usesDomain || !domain) {
|
|
554
|
-
return `${at} needs hosting.usesDomain and an org domain:
|
|
544
|
+
return `${at} needs hosting.usesDomain and an org domain: edge serves only the org's own domain`;
|
|
555
545
|
}
|
|
556
|
-
const named = Object.keys(entry.env ?? {}).find((name) =>
|
|
546
|
+
const named = Object.keys(entry.env ?? {}).find((name) => EDGE_ENTRY_ENV_NAMES.includes(name));
|
|
557
547
|
if (named)
|
|
558
548
|
return `${at} writes ${named}; remove it from env`;
|
|
559
549
|
return null;
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -51,8 +51,10 @@ export interface PlanOrgOptions {
|
|
|
51
51
|
now?: () => number;
|
|
52
52
|
/** #374: the Archon that bills this org (`plan-apply.ts`'s `orgArchonUrl`), written into Boule's env as `ARCHON_URL`. `null` with no platform wired. */
|
|
53
53
|
archonUrl?: string | null;
|
|
54
|
-
/** #318: the executor was built with a `WorkersPort`, read by `
|
|
54
|
+
/** #318: the executor was built with a `WorkersPort`, read by `planWorkers`. */
|
|
55
55
|
edgeWorkers?: boolean;
|
|
56
|
+
/** The platform's own service hostnames: `edgeReservedLabels` reserves them under the org's domain, and a wildcard Worker route (`planWorkers`) keeps them out. */
|
|
57
|
+
platformHostnames?: readonly string[];
|
|
56
58
|
}
|
|
57
59
|
export interface PlanDetail {
|
|
58
60
|
orgId: OrgId;
|
package/dist/plan/diff.js
CHANGED
|
@@ -3,10 +3,9 @@ import { APP_ENTRY_KEY, ownerMailboxRecorded, ownerMailboxes } from '../org.js';
|
|
|
3
3
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
4
|
import { BOULE_BROKER_ENTRY_KEY, bouleEnvDrift, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
|
-
import { planEdgeWorkers } from './edge-worker.js';
|
|
7
6
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
7
|
+
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
8
8
|
import { planFilesKeys } from './entry-files-key.js';
|
|
9
|
-
import { planEntryNamesKeys, planNamesReservedLabels } from './entry-names-key.js';
|
|
10
9
|
import { appHostname, entryHostnames, tunnelDnsHostnames } from './hostnames.js';
|
|
11
10
|
import { LOGIN_BINDING_SCOPE_KEY, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
12
11
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
@@ -15,6 +14,7 @@ import { DEFAULT_ORG_PLATFORM_READINESS } from './platform-org.js';
|
|
|
15
14
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
16
15
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
17
16
|
import { withStepSystem } from './step-system.js';
|
|
17
|
+
import { planWorkers } from './worker.js';
|
|
18
18
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
19
19
|
export function toPlan(detail) {
|
|
20
20
|
return {
|
|
@@ -206,14 +206,14 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
206
206
|
plannedSteps.push(...filesKeys.steps);
|
|
207
207
|
// F7 (#317): reserved labels before any entry's own key, so a label is
|
|
208
208
|
// never claimable by a customer in the gap between an entry's key going
|
|
209
|
-
// live and its siblings' labels reaching
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
plannedSteps.push(
|
|
213
|
-
// After the apps -- a
|
|
209
|
+
// live and its siblings' labels reaching edge.
|
|
210
|
+
const edgeReservedLabels = planEdgeReservedLabels(org, recordedIds, platformReadiness, options.platformHostnames ?? []);
|
|
211
|
+
if (edgeReservedLabels.step)
|
|
212
|
+
plannedSteps.push(edgeReservedLabels.step);
|
|
213
|
+
// After the apps -- a edge key needs the entry's app recorded, the same
|
|
214
214
|
// dependency `planEntryAiKeys`/`planFilesKeys` have.
|
|
215
|
-
const
|
|
216
|
-
plannedSteps.push(...
|
|
215
|
+
const entryEdgeKeys = planEntryEdgeKeys(org, recordedIds, plannedSteps, platformReadiness, (options.now ?? Date.now)());
|
|
216
|
+
plannedSteps.push(...entryEdgeKeys.steps);
|
|
217
217
|
const tunnel = planTunnel(org, byKey, claimed, recordedIds);
|
|
218
218
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
219
219
|
// one never does.
|
|
@@ -274,16 +274,16 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
274
274
|
// F8 (#318), after Agora and Boule, not right after the zone: a route
|
|
275
275
|
// already bound by a hand-made entry, another entry's route, a taken
|
|
276
276
|
// script name, or no Workers token all throw at apply, and `runSteps`
|
|
277
|
-
// stops at the first failure -- planned this early, a failing
|
|
277
|
+
// stops at the first failure -- planned this early, a failing worker step
|
|
278
278
|
// stopped Agora (and everything after it) from provisioning at all
|
|
279
279
|
// (#318 re-review). `cloudflareWorker` still resolves the zone by apex
|
|
280
280
|
// domain the same way `planRateLimit` does, so a deploy (never a
|
|
281
281
|
// retire, which needs no new zone) still waits for it.
|
|
282
|
-
const edgeWorkers =
|
|
282
|
+
const edgeWorkers = planWorkers(org, recordedIds, options.edgeWorkers ?? false, options.platformHostnames ?? []);
|
|
283
283
|
for (const step of edgeWorkers.steps) {
|
|
284
284
|
if (zoneWaiting && step.action.desired.mode === 'apply') {
|
|
285
285
|
zoneBlocked.push({
|
|
286
|
-
description: `${org.slug}'s ${step.action.key.slice(org.id.length + 1)}
|
|
286
|
+
description: `${org.slug}'s ${step.action.key.slice(org.id.length + 1)} Worker is blocked, not planned: the zone for ${org.domain} is not active yet`,
|
|
287
287
|
key: step.action.key,
|
|
288
288
|
});
|
|
289
289
|
}
|
|
@@ -312,8 +312,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
312
312
|
unmanaged.push(...filesKeys.blocked);
|
|
313
313
|
unmanaged.push(...entryAiKeys.blocked);
|
|
314
314
|
unmanaged.push(...edgeWorkers.blocked);
|
|
315
|
-
unmanaged.push(...
|
|
316
|
-
unmanaged.push(...
|
|
315
|
+
unmanaged.push(...entryEdgeKeys.blocked);
|
|
316
|
+
unmanaged.push(...edgeReservedLabels.blocked);
|
|
317
317
|
// Same for a jobs registration that cannot run (no scheduler wired) or must
|
|
318
318
|
// not run again (minted, never delivered).
|
|
319
319
|
if (jobsRegistration.blocked) {
|
|
@@ -2214,10 +2214,10 @@ function planTunnel(org, byKey, claimed, recordedIds) {
|
|
|
2214
2214
|
// differ from the names now declared. An org that never recorded a list
|
|
2215
2215
|
// has nothing to compare, so only the domain check above applies to it.
|
|
2216
2216
|
const published = recordedIds[CLOUDFLARE_TUNNEL_DNS_HOSTNAMES_SCOPE_KEY];
|
|
2217
|
-
const
|
|
2217
|
+
const edgeChanged = published !== undefined &&
|
|
2218
2218
|
org.domain !== null &&
|
|
2219
2219
|
sortedNames(published.split(',')) !== sortedNames(tunnelDnsHostnames(org, org.domain));
|
|
2220
|
-
const dnsStale = recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain ||
|
|
2220
|
+
const dnsStale = recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain || edgeChanged;
|
|
2221
2221
|
if (!connectorMissing && !dnsStale)
|
|
2222
2222
|
return null;
|
|
2223
2223
|
return {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { DeclaredOrg, HostingEntry } from '../org.js';
|
|
2
|
+
import type { DraftStep } from './diff.js';
|
|
3
|
+
import type { OrgPlatformReadiness } from './platform-org.js';
|
|
4
|
+
/**
|
|
5
|
+
* F7 (#317): one edge key per entry with `uses.edge`, minted directly
|
|
6
|
+
* below the org with the edge service (`EdgePort.mintKey({
|
|
7
|
+
* organisationId, ... })` -- no org-level root key the way files/ai have
|
|
8
|
+
* one, since the edge service takes the org id itself). `EDGE_URL` and
|
|
9
|
+
* `EDGE_API_KEY` go to the entry's env and nowhere else. A edge key has no
|
|
10
|
+
* declared renewal window in the issue, so unlike `planEntryAiKeys`/
|
|
11
|
+
* `planFilesKeys` this mints once and never renews.
|
|
12
|
+
*
|
|
13
|
+
* `EDGE_*` is written in this step, after the app's own `coolify:app`
|
|
14
|
+
* create (which never knows the key yet), so the app's first boot has
|
|
15
|
+
* neither and its first deploy restarts once this step's env write lands --
|
|
16
|
+
* the same one-extra-restart shape `planEntryAiKeys`'s `AI_*`/
|
|
17
|
+
* `planFilesKeys`'s `FILES_*` already have, not a names-specific gap.
|
|
18
|
+
*
|
|
19
|
+
* A domain change after mint is not detected (#317 re-review): the key is
|
|
20
|
+
* bound to the domain passed at mint time, but nothing here records that
|
|
21
|
+
* domain to compare against a later `org.domain`, so an org that changes
|
|
22
|
+
* domains keeps a edge key still bound to the old one. Not built --
|
|
23
|
+
* resolving it needs a manual revoke-and-re-mint (drop `uses.edge`, apply,
|
|
24
|
+
* re-add it) until this is.
|
|
25
|
+
*/
|
|
26
|
+
export declare const entryEdgeUndeliveredScopeKey: (entryKey: string) => string;
|
|
27
|
+
/** The id of a key that was live when its entry stopped declaring `uses.edge`, once handled. */
|
|
28
|
+
export declare const entryEdgeRetiredScopeKey: (entryKey: string) => string;
|
|
29
|
+
export declare const ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS = 3;
|
|
30
|
+
/** How long an entry's edge key lives. The issue names no renewal window, so this mints once and never renews. */
|
|
31
|
+
export declare const EDGE_ENTRY_KEY_DAYS = 365;
|
|
32
|
+
/** The ISO time a delivered key's mint succeeded -- the only trace of its age, since there is no renewal to track against. */
|
|
33
|
+
export declare const entryEdgeMintedAtScopeKey: (entryKey: string) => string;
|
|
34
|
+
/** A key with fewer days left than this is surfaced as a blocked warning row (#317 review): still live, but nothing here renews it. */
|
|
35
|
+
export declare const EDGE_ENTRY_KEY_WARN_WINDOW_DAYS = 30;
|
|
36
|
+
/** The reserved labels last sent to the edge service, as that sorted list's own JSON. */
|
|
37
|
+
export declare const EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY = "edge_reserved_labels_applied";
|
|
38
|
+
export declare const entryEdgeKeyName: (org: DeclaredOrg, entry: HostingEntry) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Every label under `org.domain` that foundry provisions or the platform
|
|
41
|
+
* uses, so edge never lets a company claim one: the hostnames foundry routes
|
|
42
|
+
* (`tunnelIngressHostnames`: entries, site, cms, calls, Agora, Boule, share),
|
|
43
|
+
* the names it locks (`lockedHostnames`: also the sign-in host and the
|
|
44
|
+
* platform's service hostnames), each entry's own labels, the mail labels
|
|
45
|
+
* and `www`. The entry's `*` is the space users claim in, so it reserves
|
|
46
|
+
* nothing, and neither does the bare domain.
|
|
47
|
+
*
|
|
48
|
+
* An org without a domain has no hostnames edge sees (`uses.edge` needs a
|
|
49
|
+
* domain), so it reserves `www` only.
|
|
50
|
+
*/
|
|
51
|
+
export declare function edgeReservedLabels(org: DeclaredOrg, platformHostnames?: readonly string[]): string[];
|
|
52
|
+
type Blocked = Array<{
|
|
53
|
+
description: string;
|
|
54
|
+
key: string;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* One step per entry whose edge key is missing, plus one retire step per
|
|
58
|
+
* entry whose `uses.edge` was removed but still has a recorded key.
|
|
59
|
+
* Blocked, not planned (rule 2), while the platform has no edge service
|
|
60
|
+
* wired, the entry's app is not recorded, or a previous mint's delivery
|
|
61
|
+
* failed.
|
|
62
|
+
*/
|
|
63
|
+
export declare function planEntryEdgeKeys(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], readiness: OrgPlatformReadiness, now?: number): {
|
|
64
|
+
steps: DraftStep[];
|
|
65
|
+
blocked: Blocked;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The reserved-labels step: resent whenever the org's own label set
|
|
69
|
+
* (`edgeReservedLabels`) differs from what was last sent. While the org
|
|
70
|
+
* uses edge at all (at least one entry with `uses.edge`) that set is
|
|
71
|
+
* `edgeReservedLabels(org)`; once the last entry drops `uses.edge`, this
|
|
72
|
+
* clears whatever was last sent (an empty list) rather than leaving it
|
|
73
|
+
* stuck in names forever -- a one-time step, same as any other change.
|
|
74
|
+
*/
|
|
75
|
+
export declare function planEdgeReservedLabels(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, readiness: OrgPlatformReadiness, platformHostnames?: readonly string[]): {
|
|
76
|
+
step: DraftStep | null;
|
|
77
|
+
blocked: Blocked;
|
|
78
|
+
};
|
|
79
|
+
export {};
|