@wtfalch/foundry 0.16.14 → 0.16.16
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/org.d.ts +28 -5
- package/dist/org.js +19 -0
- package/dist/plan/deploy-health.d.ts +136 -0
- package/dist/plan/deploy-health.js +153 -0
- package/dist/plan/diff.js +50 -9
- package/dist/plan/entry-files-key.d.ts +3 -1
- package/dist/plan/entry-files-key.js +7 -10
- package/dist/plan/executor.d.ts +52 -5
- package/dist/plan/executor.js +79 -6
- package/dist/plan/hostnames.d.ts +6 -0
- package/dist/plan/hostnames.js +13 -0
- package/dist/plan/index.d.ts +1 -1
- package/dist/plan/index.js +1 -1
- package/dist/plan/plan-apply.d.ts +17 -0
- package/dist/plan/plan-apply.js +48 -3
- package/dist/plan/platform-diff.d.ts +89 -9
- package/dist/plan/platform-diff.js +176 -13
- package/dist/plan/platform-org.d.ts +9 -0
- package/dist/plan/platform-providers.d.ts +46 -5
- package/dist/plan/platform-providers.js +539 -104
- package/package.json +2 -2
package/dist/plan/executor.d.ts
CHANGED
|
@@ -294,10 +294,13 @@ export interface CoolifyPort {
|
|
|
294
294
|
* fresh Synapse/MAS config file, since a file-storage write alone does not
|
|
295
295
|
* reach the running container until its next deploy (issue #128's "a
|
|
296
296
|
* running app is redeployed when its config changed"). Fire-and-forget,
|
|
297
|
-
* like `restart` above:
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* deploy` method-for-method.
|
|
297
|
+
* like `restart` above: `coolifyCalls` only calls this when the config it
|
|
298
|
+
* just wrote was actually freshly minted, with nothing recorded afterward
|
|
299
|
+
* that depends on the new version actually being live. Matches
|
|
300
|
+
* `CoolifyClient.deploy` method-for-method. A step that records an applied
|
|
301
|
+
* hostname/gitRef after a deploy uses {@link CoolifyPort.deployAndWait}
|
|
302
|
+
* instead (#252) — `deploy-health.ts`'s `waitForNewDeployment` is the one
|
|
303
|
+
* shared helper every such step calls, never this method directly.
|
|
301
304
|
*/
|
|
302
305
|
deploy(uuid: string, force?: boolean): Promise<unknown>;
|
|
303
306
|
/**
|
|
@@ -361,6 +364,27 @@ export interface CoolifyPort {
|
|
|
361
364
|
}): Promise<{
|
|
362
365
|
uuid: string;
|
|
363
366
|
}>;
|
|
367
|
+
/**
|
|
368
|
+
* Deploys and blocks until Coolify's own deployment record reaches a
|
|
369
|
+
* terminal state (#252) — matches `CoolifyClient.deployAndWait`
|
|
370
|
+
* method-for-method. Unlike `deploy` above, or unlike polling
|
|
371
|
+
* `getApplication`'s live `status`, this is the one honest answer to
|
|
372
|
+
* "did the new version actually land": a failed build leaves the previous
|
|
373
|
+
* container running and healthy, so `status` and a served request read
|
|
374
|
+
* identically whether the deploy succeeded or not (`CoolifyDeploymentRecord`'s
|
|
375
|
+
* own doc comment in `@wtfalch/providers`). `force` defaults to `true` on
|
|
376
|
+
* the real client for the same reason its own doc comment gives — every
|
|
377
|
+
* caller here is provisioning or releasing, where a wrong "it's applied"
|
|
378
|
+
* costs more than a warm build cache.
|
|
379
|
+
*/
|
|
380
|
+
deployAndWait(uuid: string, options?: {
|
|
381
|
+
force?: boolean;
|
|
382
|
+
attempts?: number;
|
|
383
|
+
intervalMs?: number;
|
|
384
|
+
}): Promise<{
|
|
385
|
+
deploymentUuid: string;
|
|
386
|
+
status: string;
|
|
387
|
+
}>;
|
|
364
388
|
}
|
|
365
389
|
/**
|
|
366
390
|
* The Cloudflare calls this executor makes, shaped to match
|
|
@@ -812,7 +836,7 @@ export interface ZitadelPort {
|
|
|
812
836
|
givenName: string;
|
|
813
837
|
familyName: string;
|
|
814
838
|
email: string;
|
|
815
|
-
}): Promise<{
|
|
839
|
+
}, urlTemplate?: string): Promise<{
|
|
816
840
|
userId: string;
|
|
817
841
|
created: boolean;
|
|
818
842
|
}>;
|
|
@@ -1341,6 +1365,16 @@ export interface FoundryStepExecutorPorts {
|
|
|
1341
1365
|
* themselves.
|
|
1342
1366
|
*/
|
|
1343
1367
|
deliverSecret(ref: string, secret: string): Promise<void>;
|
|
1368
|
+
/**
|
|
1369
|
+
* Archon's held keys, for an entry env from `sharedSecret` (read, else
|
|
1370
|
+
* generate and `put`) and `platformMailKey` (read only). Optional: without
|
|
1371
|
+
* it, or without `put`, such an env is omitted and the plan reports it
|
|
1372
|
+
* blocked. A value goes to the entry's env and nowhere else.
|
|
1373
|
+
*/
|
|
1374
|
+
heldKeys?: {
|
|
1375
|
+
resolve(ref: string): Promise<string | null | undefined>;
|
|
1376
|
+
put?(ref: string, value: string): Promise<void>;
|
|
1377
|
+
};
|
|
1344
1378
|
/**
|
|
1345
1379
|
* Read access to the declared-org store, narrowed to the one query
|
|
1346
1380
|
* `cloudflareZone` needs for its cross-org refusal check (#131) -- see that
|
|
@@ -1636,6 +1670,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1636
1670
|
* provisioned, where this one is unconditional (#239).
|
|
1637
1671
|
*/
|
|
1638
1672
|
private pushAppEnv;
|
|
1673
|
+
/**
|
|
1674
|
+
* The org's shared secret `name`: read from the held keys, else generated
|
|
1675
|
+
* and held first, so a crash after the `put` and before the env write
|
|
1676
|
+
* hands the same value out on the next apply. Only called when `put` exists.
|
|
1677
|
+
* An error here names the ref, never a value.
|
|
1678
|
+
*/
|
|
1679
|
+
private ensureSharedSecret;
|
|
1639
1680
|
/**
|
|
1640
1681
|
* F5: the app's `AUTH_APP_KEY`, minted once and only when the app has none
|
|
1641
1682
|
* of at least 32 characters (the broker refuses a shorter one). `null` once
|
|
@@ -2360,6 +2401,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2360
2401
|
* this is called every time regardless of whether it already ran --
|
|
2361
2402
|
* idempotent by find-or-create, not by checking `recordedIds` first.
|
|
2362
2403
|
*
|
|
2404
|
+
* F6b: `action.desired.shareHost` (`diff.ts`'s `planFilesOrganisation`) is
|
|
2405
|
+
* passed through every time, even though `entry-files-key.ts` reports the
|
|
2406
|
+
* route itself as blocked until the platform's files host can serve
|
|
2407
|
+
* `share.<domain>` -- this is the one place the name reaches files at all,
|
|
2408
|
+
* so files can act on it the moment its own route exists.
|
|
2409
|
+
*
|
|
2363
2410
|
* #273: also ensures the org's Agora origin is on the files host's CORS
|
|
2364
2411
|
* allowlist (`FilesOrganisationPort.ensureAllowedOrigin`), the same
|
|
2365
2412
|
* repeat-is-a-no-op shape as `provisionOrganisation` itself -- every org
|
package/dist/plan/executor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
3
|
import { CALLS_SFU_PRIVATE_URL, CALLS_SFU_PUBLIC_URL } from '../calls-sfu.js';
|
|
4
|
-
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, } from '../org.js';
|
|
4
|
+
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
5
5
|
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
6
6
|
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
7
7
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
@@ -14,6 +14,7 @@ import { appHostname, coolifyDomains, entryHostnames, tunnelDnsHostnames, tunnel
|
|
|
14
14
|
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, loginBindingState, loginHostClientIds, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
15
15
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
16
16
|
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, appDomainsAppliedScopeKey, appEnvAppliedScopeKey, appNameFor, appSettingsAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, } from './platform-diff.js';
|
|
17
|
+
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
17
18
|
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryProxyLabels, sameLabelSets, } from './proxy-labels.js';
|
|
18
19
|
/**
|
|
19
20
|
* #274 fix round 4: bridges `@wtfalch/providers`' real `ZitadelClient.
|
|
@@ -861,11 +862,16 @@ export class FoundryStepExecutor {
|
|
|
861
862
|
if (!zitadelOrgId) {
|
|
862
863
|
throw new Error(`zitadel:owner for ${org.slug}: no ZITADEL org recorded yet -- the issuer must be provisioned first`);
|
|
863
864
|
}
|
|
865
|
+
// ZITADEL's default invite link (`/ui/v2/login/verify`) 404s; the platform's login app, which
|
|
866
|
+
// every org's users sign in through on this instance, serves `/verify` at the root.
|
|
867
|
+
const authIssuer = action.desired.authIssuer;
|
|
864
868
|
const { userId } = await this.ports.zitadel.inviteOrgOwner(zitadelOrgId, {
|
|
865
869
|
givenName: action.desired.givenName,
|
|
866
870
|
familyName: action.desired.familyName,
|
|
867
871
|
email: action.desired.email,
|
|
868
|
-
}
|
|
872
|
+
}, authIssuer
|
|
873
|
+
? `${authIssuer}/verify?userId={{.UserID}}&code={{.Code}}&organization={{.OrgID}}&invite=true`
|
|
874
|
+
: undefined);
|
|
869
875
|
// An owner invited after chat was built never reaches `coolifyChat`
|
|
870
876
|
// again, so the role is granted here once the messaging project exists.
|
|
871
877
|
const messagingProjectId = context.recordedIds()[ZITADEL_MESSAGING_PROJECT_SCOPE_KEY];
|
|
@@ -1424,7 +1430,13 @@ export class FoundryStepExecutor {
|
|
|
1424
1430
|
// The app receives whatever it needs to sign in (gap P2). Unconditional,
|
|
1425
1431
|
// unlike the database push above: every app has a hostname, and an
|
|
1426
1432
|
// upsert of the same value on a later apply is a no-op, not a change.
|
|
1427
|
-
const appEnv = await this.pushAppEnv(appId, org, entry, context, action.desired.authIssuer ?? null
|
|
1433
|
+
const appEnv = await this.pushAppEnv(appId, org, entry, context, action.desired.authIssuer ?? null,
|
|
1434
|
+
// What the plan decided is available (diff.ts `planApps`), so the marker
|
|
1435
|
+
// recorded below is the one the plan compares against. Absent: none.
|
|
1436
|
+
action.desired.secretSources ?? {
|
|
1437
|
+
platformMailKey: false,
|
|
1438
|
+
sharedSecret: false,
|
|
1439
|
+
});
|
|
1428
1440
|
// Rule 4 (#239, #243): a container reads its env only at start, so an app
|
|
1429
1441
|
// that was already there is restarted, once, when its database URLs or its
|
|
1430
1442
|
// declared appEnv changed.
|
|
@@ -1633,7 +1645,7 @@ export class FoundryStepExecutor {
|
|
|
1633
1645
|
* that call runs only once the `app` entry's `database` is declared and
|
|
1634
1646
|
* provisioned, where this one is unconditional (#239).
|
|
1635
1647
|
*/
|
|
1636
|
-
async pushAppEnv(appId, org, entry, context, authIssuer) {
|
|
1648
|
+
async pushAppEnv(appId, org, entry, context, authIssuer, secretSources) {
|
|
1637
1649
|
const recorded = context.recordedIds();
|
|
1638
1650
|
const envs = [
|
|
1639
1651
|
// Only the `app` entry has a public name; the others are internal.
|
|
@@ -1691,14 +1703,44 @@ export class FoundryStepExecutor {
|
|
|
1691
1703
|
// minted here; a source with no value yet (its own step has not run,
|
|
1692
1704
|
// or has not run for this apply) is simply omitted, the same way every
|
|
1693
1705
|
// other optional entry above is.
|
|
1694
|
-
|
|
1706
|
+
// The secret sources (`sharedSecret`, `platformMailKey`) are filled here
|
|
1707
|
+
// only, straight into `envs`: never recorded, returned or logged.
|
|
1708
|
+
const heldKeys = this.ports.heldKeys;
|
|
1709
|
+
// Availability is the plan's (`secretSources`). If the plan said a key is
|
|
1710
|
+
// held and this port cannot read it, the host wired the two differently:
|
|
1711
|
+
// fail by ref, never push a marker for an env that was not written.
|
|
1712
|
+
let mailKey = null;
|
|
1713
|
+
if (secretSources.platformMailKey) {
|
|
1714
|
+
try {
|
|
1715
|
+
mailKey = (await heldKeys?.resolve(PLATFORM_AUTH_BROKER_MAIL_KEY_REF)) || null;
|
|
1716
|
+
}
|
|
1717
|
+
catch {
|
|
1718
|
+
// reported below, by ref
|
|
1719
|
+
}
|
|
1720
|
+
if (mailKey === null) {
|
|
1721
|
+
throw new Error(`coolify:app for ${org.slug}: the plan found ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF} held but the executor's heldKeys cannot read it`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
if (secretSources.sharedSecret && typeof heldKeys?.put !== 'function') {
|
|
1725
|
+
throw new Error(`coolify:app for ${org.slug}: the plan found held keys that can put but the executor's heldKeys cannot`);
|
|
1726
|
+
}
|
|
1727
|
+
const resolved = resolveAppEnv(org, entry, recorded, secretSources);
|
|
1728
|
+
const appEnv = resolved.values;
|
|
1695
1729
|
for (const [key, value] of Object.entries(appEnv))
|
|
1696
1730
|
envs.push({ key, value });
|
|
1731
|
+
for (const { name, source } of resolved.secrets) {
|
|
1732
|
+
envs.push({
|
|
1733
|
+
key: name,
|
|
1734
|
+
value: source === 'platformMailKey'
|
|
1735
|
+
? mailKey
|
|
1736
|
+
: await this.ensureSharedSecret(org, source.name, recorded),
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1697
1739
|
await this.ports.coolify.bulkUpsertEnvs(appId, envs);
|
|
1698
1740
|
// `coolifyApp` records `applied` once the restart succeeded, so `planApp`
|
|
1699
1741
|
// replans this step when the declared map changes (rule 3). Ids and a
|
|
1700
1742
|
// domain, never a secret.
|
|
1701
|
-
const applied = JSON.stringify(
|
|
1743
|
+
const applied = JSON.stringify(resolved.marker);
|
|
1702
1744
|
// The sign-in values, recorded by `coolifyApp` the same way; the key is
|
|
1703
1745
|
// never part of them.
|
|
1704
1746
|
const authApplied = JSON.stringify(authEnv);
|
|
@@ -1710,6 +1752,28 @@ export class FoundryStepExecutor {
|
|
|
1710
1752
|
authApplied,
|
|
1711
1753
|
};
|
|
1712
1754
|
}
|
|
1755
|
+
/**
|
|
1756
|
+
* The org's shared secret `name`: read from the held keys, else generated
|
|
1757
|
+
* and held first, so a crash after the `put` and before the env write
|
|
1758
|
+
* hands the same value out on the next apply. Only called when `put` exists.
|
|
1759
|
+
* An error here names the ref, never a value.
|
|
1760
|
+
*/
|
|
1761
|
+
async ensureSharedSecret(org, name, recorded) {
|
|
1762
|
+
const heldKeys = this.ports.heldKeys;
|
|
1763
|
+
const ref = sharedSecretRef(org.id, name);
|
|
1764
|
+
const held = await heldKeys?.resolve(ref);
|
|
1765
|
+
if (held)
|
|
1766
|
+
return held;
|
|
1767
|
+
// Another entry (or this one) already got a value. Generating again would
|
|
1768
|
+
// split them, so a lost value needs a person, not a new mint.
|
|
1769
|
+
const label = `"secret:sharedSecret:${name}"`;
|
|
1770
|
+
if (Object.entries(recorded).some(([key, value]) => key.startsWith('app_env_applied.') && value.includes(label))) {
|
|
1771
|
+
throw new Error(`coolify:app for ${org.slug}: shared secret ${name} was delivered but is no longer held (ref ${ref}); restore the held value, or clear it from every entry's env and applied marker to mint a new one`);
|
|
1772
|
+
}
|
|
1773
|
+
const value = randomBytes(32).toString('base64url');
|
|
1774
|
+
await heldKeys?.put?.(ref, value);
|
|
1775
|
+
return value;
|
|
1776
|
+
}
|
|
1713
1777
|
/**
|
|
1714
1778
|
* F5: the app's `AUTH_APP_KEY`, minted once and only when the app has none
|
|
1715
1779
|
* of at least 32 characters (the broker refuses a shorter one). `null` once
|
|
@@ -3971,6 +4035,12 @@ export class FoundryStepExecutor {
|
|
|
3971
4035
|
* this is called every time regardless of whether it already ran --
|
|
3972
4036
|
* idempotent by find-or-create, not by checking `recordedIds` first.
|
|
3973
4037
|
*
|
|
4038
|
+
* F6b: `action.desired.shareHost` (`diff.ts`'s `planFilesOrganisation`) is
|
|
4039
|
+
* passed through every time, even though `entry-files-key.ts` reports the
|
|
4040
|
+
* route itself as blocked until the platform's files host can serve
|
|
4041
|
+
* `share.<domain>` -- this is the one place the name reaches files at all,
|
|
4042
|
+
* so files can act on it the moment its own route exists.
|
|
4043
|
+
*
|
|
3974
4044
|
* #273: also ensures the org's Agora origin is on the files host's CORS
|
|
3975
4045
|
* allowlist (`FilesOrganisationPort.ensureAllowedOrigin`), the same
|
|
3976
4046
|
* repeat-is-a-no-op shape as `provisionOrganisation` itself -- every org
|
|
@@ -3989,6 +4059,7 @@ export class FoundryStepExecutor {
|
|
|
3989
4059
|
organisationId: org.id,
|
|
3990
4060
|
ceilingBytes: action.desired.ceilingBytes ?? null,
|
|
3991
4061
|
ceilingMicros: action.desired.ceilingMicros ?? null,
|
|
4062
|
+
shareHost: action.desired.shareHost ?? null,
|
|
3992
4063
|
...(owner ? { name: org.displayName, owner } : {}),
|
|
3993
4064
|
});
|
|
3994
4065
|
const agoraOrigin = action.desired.agoraOrigin;
|
|
@@ -4429,6 +4500,7 @@ export class FoundryStepExecutor {
|
|
|
4429
4500
|
const envs = [
|
|
4430
4501
|
{ key: 'FILES_URL', value: d.filesUrl },
|
|
4431
4502
|
{ key: 'FILES_ORGANISATION_ID', value: org.id },
|
|
4503
|
+
...(d.shareOrigin ? [{ key: 'FILES_SHARE_ORIGIN', value: d.shareOrigin }] : []),
|
|
4432
4504
|
];
|
|
4433
4505
|
for (const name of d.collections) {
|
|
4434
4506
|
let collection;
|
|
@@ -4495,6 +4567,7 @@ export class FoundryStepExecutor {
|
|
|
4495
4567
|
permissions: d.permissions,
|
|
4496
4568
|
collections: d.collections,
|
|
4497
4569
|
filesUrl: d.filesUrl,
|
|
4570
|
+
...(d.shareOrigin ? { shareOrigin: d.shareOrigin } : {}),
|
|
4498
4571
|
}));
|
|
4499
4572
|
if (expiresAt && previousKeyId && previousKeyId !== resourceId) {
|
|
4500
4573
|
await this.revokeFilesEntryKey(files, org, entryKey, previousKeyId, context);
|
package/dist/plan/hostnames.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export declare function appHostname(org: DeclaredOrg): string;
|
|
|
26
26
|
export declare function coolifyDomains(names: readonly string[]): string;
|
|
27
27
|
/** What `domains` held on an `app` entry's Coolify app before entries had hostnames. */
|
|
28
28
|
export declare function legacyAppHostname(org: DeclaredOrg): string;
|
|
29
|
+
/**
|
|
30
|
+
* `share.<domain>` when hosting is on and an entry declares
|
|
31
|
+
* `uses.files.shareHost`, else `null`. The files service answers public
|
|
32
|
+
* shares there; foundry never puts it behind Access.
|
|
33
|
+
*/
|
|
34
|
+
export declare function filesShareHostname(org: DeclaredOrg): string | null;
|
|
29
35
|
/**
|
|
30
36
|
* The hostnames `cloudflareTunnel` routes to Coolify's proxy (#115, #145,
|
|
31
37
|
* #164): a site's two names, every entry's names, an own-domain calls name,
|
package/dist/plan/hostnames.js
CHANGED
|
@@ -47,6 +47,16 @@ export function coolifyDomains(names) {
|
|
|
47
47
|
export function legacyAppHostname(org) {
|
|
48
48
|
return org.hosting.usesDomain && org.domain ? org.domain : `${org.slug}.oikos.host`;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* `share.<domain>` when hosting is on and an entry declares
|
|
52
|
+
* `uses.files.shareHost`, else `null`. The files service answers public
|
|
53
|
+
* shares there; foundry never puts it behind Access.
|
|
54
|
+
*/
|
|
55
|
+
export function filesShareHostname(org) {
|
|
56
|
+
if (!org.hosting.enabled || !org.domain)
|
|
57
|
+
return null;
|
|
58
|
+
return org.hosting.entries.some((e) => e.uses?.files?.shareHost) ? `share.${org.domain}` : null;
|
|
59
|
+
}
|
|
50
60
|
/**
|
|
51
61
|
* The hostnames `cloudflareTunnel` routes to Coolify's proxy (#115, #145,
|
|
52
62
|
* #164): a site's two names, every entry's names, an own-domain calls name,
|
|
@@ -71,6 +81,9 @@ export function tunnelIngressHostnames(org) {
|
|
|
71
81
|
// #161: and Boule moves onto `boule.<domain>` the same way.
|
|
72
82
|
hostnames.push(bouleHostnameFor(org));
|
|
73
83
|
}
|
|
84
|
+
const share = filesShareHostname(org);
|
|
85
|
+
if (share)
|
|
86
|
+
hostnames.push(share);
|
|
74
87
|
// The wildcard goes last: Cloudflare takes the first ingress rule that
|
|
75
88
|
// matches, so every exact name above must come before `*.<domain>`.
|
|
76
89
|
const wildcard = wildcardHostname(org);
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type { PlatformOrgClientFactories } from './platform-org-ports.js';
|
|
|
10
10
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
11
11
|
export type { AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, } from './plan-apply.js';
|
|
12
12
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
13
|
-
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
|
|
13
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
14
14
|
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
|
|
15
15
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
16
16
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { planPlatform, toPlatformPlan, JOBS_SCHEDULER_KEY, PLATFORM_AI_KEY, PLAT
|
|
|
4
4
|
export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
5
5
|
export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.js';
|
|
6
6
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
7
|
-
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
|
|
7
|
+
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, filesPublicHostsValue, filesShareDomains, } from './platform-providers.js';
|
|
8
8
|
// #171: how a host signs files management requests with the delivered key.
|
|
9
9
|
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
10
10
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
@@ -210,6 +210,12 @@ export interface PlatformStepExecutor {
|
|
|
210
210
|
/** #257 fix round 4: see `PlanPlatformOptions.drifted.backupStorageBlocked` (`platform-diff.ts`). */
|
|
211
211
|
backupStorageBlocked?: string;
|
|
212
212
|
}>;
|
|
213
|
+
/**
|
|
214
|
+
* Gives the platform files runner a changed share-domain set and touches
|
|
215
|
+
* nothing else. Returns the scope ids to record (only the share-domains
|
|
216
|
+
* one), or `null` when nothing changed.
|
|
217
|
+
*/
|
|
218
|
+
refreshFilesShare?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
|
|
213
219
|
execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
|
|
214
220
|
resourceId?: string;
|
|
215
221
|
} | undefined>;
|
|
@@ -378,6 +384,17 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
378
384
|
constructor(options: FoundryPlanApplyOptions);
|
|
379
385
|
plan(id: OrgId): Promise<Plan>;
|
|
380
386
|
apply(plan: Plan, confirmedOrgName: string): Promise<AppliedState>;
|
|
387
|
+
/**
|
|
388
|
+
* After an org apply: gives the platform files runner a changed share-domain
|
|
389
|
+
* set (its Coolify `domains` and `FILES_PUBLIC_HOSTS`, derived from the
|
|
390
|
+
* declared orgs) without a platform pass. Touches only the share hosts and
|
|
391
|
+
* redeploys the runner; the platform files step itself never runs here.
|
|
392
|
+
* Skips quietly for an actor who may not apply the platform, and when the
|
|
393
|
+
* platform files step was never applied. There is no platform lock, so the
|
|
394
|
+
* service ids are re-read just before the write and only the share-domains
|
|
395
|
+
* key is recorded. A failure is rethrown naming that the org part succeeded.
|
|
396
|
+
*/
|
|
397
|
+
private refreshPlatformFilesShare;
|
|
381
398
|
/**
|
|
382
399
|
* #167: {@link plan}, for the shared platform (#166) rather than an org --
|
|
383
400
|
* there is no id to pass, since `DeclaredPlatform` is one singleton record.
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -8,8 +8,9 @@ import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
|
8
8
|
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
9
9
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
10
10
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
11
|
-
import { PLATFORM_ARCHON_KEY, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
11
|
+
import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
12
12
|
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
13
|
+
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
13
14
|
export class DeclaredOrgNotFoundError extends Error {
|
|
14
15
|
constructor(orgId) {
|
|
15
16
|
super(`no declared org ${orgId}`);
|
|
@@ -139,7 +140,42 @@ export class FoundryPlanApply {
|
|
|
139
140
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
140
141
|
throw new ConfirmationRequiredError(orgId);
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
+
const applied = await this.runSteps(org, plan, detail);
|
|
144
|
+
await this.refreshPlatformFilesShare();
|
|
145
|
+
return applied;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* After an org apply: gives the platform files runner a changed share-domain
|
|
149
|
+
* set (its Coolify `domains` and `FILES_PUBLIC_HOSTS`, derived from the
|
|
150
|
+
* declared orgs) without a platform pass. Touches only the share hosts and
|
|
151
|
+
* redeploys the runner; the platform files step itself never runs here.
|
|
152
|
+
* Skips quietly for an actor who may not apply the platform, and when the
|
|
153
|
+
* platform files step was never applied. There is no platform lock, so the
|
|
154
|
+
* service ids are re-read just before the write and only the share-domains
|
|
155
|
+
* key is recorded. A failure is rethrown naming that the org part succeeded.
|
|
156
|
+
*/
|
|
157
|
+
async refreshPlatformFilesShare() {
|
|
158
|
+
const deps = this.platformDeps;
|
|
159
|
+
if (!deps?.executor.refreshFilesShare)
|
|
160
|
+
return;
|
|
161
|
+
if ((await this.authz.canApplyPlatform?.(this.actor)) !== true)
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
const platform = await deps.store.get();
|
|
165
|
+
if (!platform?.files.enabled)
|
|
166
|
+
return;
|
|
167
|
+
const recorded = await deps.store.getServiceIds();
|
|
168
|
+
if (!recorded[`coolify:app:${PLATFORM_FILES_KEY}`] ||
|
|
169
|
+
!recorded[PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY])
|
|
170
|
+
return;
|
|
171
|
+
const scopeIds = await deps.executor.refreshFilesShare(platform, recorded);
|
|
172
|
+
if (scopeIds === null)
|
|
173
|
+
return;
|
|
174
|
+
await deps.store.putServiceIds({ ...(await deps.store.getServiceIds()), ...scopeIds });
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
throw new Error(`org applied; platform files share refresh failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
178
|
+
}
|
|
143
179
|
}
|
|
144
180
|
/**
|
|
145
181
|
* #167: {@link plan}, for the shared platform (#166) rather than an org --
|
|
@@ -443,7 +479,16 @@ export class FoundryPlanApply {
|
|
|
443
479
|
refs.add(ORG_PLATFORM_KEY_REFS.ai);
|
|
444
480
|
if (this.jobsScheduler)
|
|
445
481
|
refs.add(ORG_PLATFORM_KEY_REFS.jobs);
|
|
446
|
-
|
|
482
|
+
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys);
|
|
483
|
+
// Entry env from the platform's mail key or a shared secret (#T8).
|
|
484
|
+
let mailKeyHeld = false;
|
|
485
|
+
try {
|
|
486
|
+
mailKeyHeld = Boolean(await heldKeys?.resolve(PLATFORM_AUTH_BROKER_MAIL_KEY_REF));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// not held
|
|
490
|
+
}
|
|
491
|
+
return { ...readiness, mailKeyHeld, canHoldSecrets: typeof heldKeys?.put === 'function' };
|
|
447
492
|
}
|
|
448
493
|
/**
|
|
449
494
|
* #177: `AgoraHostValues` from the applied platform, the same
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
|
|
1
|
+
import type { DeclaredOrg, EnvSharedSecret, HostingEntry, OrgId } from '../org.js';
|
|
2
2
|
import { type Plan, type UnmanagedItem, type VersionChangeItem } from '../plan.js';
|
|
3
3
|
import type { DeclaredPlatform } from '../platform.js';
|
|
4
4
|
import type { PlannedStep } from './diff.js';
|
|
@@ -107,6 +107,17 @@ export declare const PLATFORM_ARCHON_KEY = "platform:archon";
|
|
|
107
107
|
*/
|
|
108
108
|
export declare const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = "archon_applied_hostname";
|
|
109
109
|
export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied_git_ref";
|
|
110
|
+
/**
|
|
111
|
+
* #398: the integrations management key id Archon runs with, recorded by
|
|
112
|
+
* `provisionPlatformArchon` only after its deployment finished and its health
|
|
113
|
+
* check passed with that id in its env. `provisionPlatformIntegrations`'s
|
|
114
|
+
* rotation step reads this before dropping a previous key id from the
|
|
115
|
+
* runner's `MANAGEMENT_PUBLIC_KEYS`: the old key stays pinned until this
|
|
116
|
+
* matches the new id, so a request signed by an Archon container still
|
|
117
|
+
* running the old key is never refused. If Archon's deploy fails this stays
|
|
118
|
+
* unrecorded and the old key stays pinned.
|
|
119
|
+
*/
|
|
120
|
+
export declare const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = "archon_integrations_key_synced";
|
|
110
121
|
/**
|
|
111
122
|
* #211 round 4: the broker app id Archon last registered its entry with.
|
|
112
123
|
* `provisionPlatformArchon` reads the broker to register with from
|
|
@@ -118,6 +129,19 @@ export declare const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = "archon_applied
|
|
|
118
129
|
* changed hostname or `gitRef` does.
|
|
119
130
|
*/
|
|
120
131
|
export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_broker_app";
|
|
132
|
+
/**
|
|
133
|
+
* The `AUTHZ_BOOTSTRAP_OPERATOR` the last Archon apply set (`none` when it set
|
|
134
|
+
* none). `planArchon` plans the step again when the owner's id recorded by
|
|
135
|
+
* `platform:auth` differs from it, so Archon picks the owner up once auth
|
|
136
|
+
* has recorded it.
|
|
137
|
+
*/
|
|
138
|
+
export declare const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = "archon_applied_bootstrap_operator";
|
|
139
|
+
/**
|
|
140
|
+
* The owner's ZITADEL user id Archon takes as its first operator, from
|
|
141
|
+
* {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
|
|
142
|
+
* recorded or the recorded value is `none` (no owner declared).
|
|
143
|
+
*/
|
|
144
|
+
export declare function archonBootstrapOperator(recorded: Readonly<Record<string, string>>): string | undefined;
|
|
121
145
|
/**
|
|
122
146
|
* #175: set to '1' before the step points the tunnel at `http_status:404`
|
|
123
147
|
* (Access app rebuild), cleared to '' once the real ingress is written back.
|
|
@@ -400,14 +424,31 @@ export declare function appDomainsAppliedScopeKey(entryKey: string): string;
|
|
|
400
424
|
* entry), so an app that runs today plans no change.
|
|
401
425
|
*/
|
|
402
426
|
export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
|
|
427
|
+
/** Whether the secret sources can be filled right now: the mail key is held, and a new shared secret can be held. */
|
|
428
|
+
export interface AppSecretAvailability {
|
|
429
|
+
platformMailKey: boolean;
|
|
430
|
+
sharedSecret: boolean;
|
|
431
|
+
}
|
|
403
432
|
/**
|
|
404
433
|
* The entry's `env` values that have a value, in env-name order, and the env
|
|
405
434
|
* names whose source has none yet. Shared by `planApp` and the executor's
|
|
406
435
|
* `pushAppEnv` so both read the same recorded ids.
|
|
436
|
+
*
|
|
437
|
+
* A secret source is never a value here. `secrets` names each one that can be
|
|
438
|
+
* filled (`available`), `unresolvedSecrets` each that cannot, and `marker`
|
|
439
|
+
* is `values` plus a `secret:<source>` label per available secret: the JSON
|
|
440
|
+
* `coolifyApp` records as applied, so a declared change replans and no secret
|
|
441
|
+
* is written into it.
|
|
407
442
|
*/
|
|
408
|
-
export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string
|
|
443
|
+
export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, available?: AppSecretAvailability): {
|
|
409
444
|
values: Record<string, string>;
|
|
410
445
|
unresolved: Array<[string, string]>;
|
|
446
|
+
secrets: Array<{
|
|
447
|
+
name: string;
|
|
448
|
+
source: 'platformMailKey' | EnvSharedSecret;
|
|
449
|
+
}>;
|
|
450
|
+
unresolvedSecrets: Array<[string, string]>;
|
|
451
|
+
marker: Record<string, string>;
|
|
411
452
|
};
|
|
412
453
|
/**
|
|
413
454
|
* #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
|
|
@@ -478,13 +519,6 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
|
|
|
478
519
|
* that throws) is repaired by the next one.
|
|
479
520
|
*/
|
|
480
521
|
export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
|
|
481
|
-
/**
|
|
482
|
-
* The Coolify deployment queued for the host (its `deployment_uuid`, or
|
|
483
|
-
* `untracked` when the deploy answered none), set when that deploy is queued
|
|
484
|
-
* and cleared once it finished. The mint waits for it, so a retry after a
|
|
485
|
-
* failed wait still waits instead of minting against the old container.
|
|
486
|
-
*/
|
|
487
|
-
export declare const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = "ai_platform_runner_deployment";
|
|
488
522
|
/**
|
|
489
523
|
* #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
|
|
490
524
|
* dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
|
|
@@ -684,3 +718,49 @@ export declare const PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY = "integrations_man
|
|
|
684
718
|
/** The hostname and gitRef the last successful apply put live; a difference from the declared ones plans the step again. */
|
|
685
719
|
export declare const PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY = "integrations_applied_hostname";
|
|
686
720
|
export declare const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = "integrations_applied_gitref";
|
|
721
|
+
/**
|
|
722
|
+
* #398: the key id currently pinned on the runner as the active entry of
|
|
723
|
+
* `MANAGEMENT_PUBLIC_KEYS` -- `foundry-1` on the first mint, and whatever a
|
|
724
|
+
* rotation minted next after that. `collectArchonServiceInputs` reads this
|
|
725
|
+
* (falling back to the fixed `INTEGRATIONS_MANAGEMENT_KEY_ID` constant for a
|
|
726
|
+
* platform applied before this key existed) for Archon's own
|
|
727
|
+
* `INTEGRATIONS_MANAGEMENT_KEY_ID` env, so a rotated id reaches it without
|
|
728
|
+
* Archon's step needing to know anything happened on the integrations side.
|
|
729
|
+
*/
|
|
730
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY = "integrations_management_key_id";
|
|
731
|
+
/**
|
|
732
|
+
* #398: the key id the runner was last deployed with in its
|
|
733
|
+
* `MANAGEMENT_PUBLIC_KEYS`, recorded by `provisionPlatformIntegrations` only
|
|
734
|
+
* after the runner's deployment finished. Archon is not moved to a new key id
|
|
735
|
+
* until this equals the active id, so Archon never signs with a key the
|
|
736
|
+
* running runner does not yet hold.
|
|
737
|
+
*/
|
|
738
|
+
export declare const PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY = "integrations_runner_keyring_deployed";
|
|
739
|
+
/**
|
|
740
|
+
* #398: the same two-key discipline as
|
|
741
|
+
* {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}, for a rotation's
|
|
742
|
+
* new pair: recorded the moment it is minted, before delivery, and cleared
|
|
743
|
+
* by nothing -- {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} moving past it
|
|
744
|
+
* is what later runs read as "delivered and pinned", the same way the
|
|
745
|
+
* once-only mint's guard is a second key rather than removing the first.
|
|
746
|
+
* An id here that {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} has not
|
|
747
|
+
* caught up to means the pair may be lost or half-applied; the step reports
|
|
748
|
+
* it and mints no further pair.
|
|
749
|
+
*/
|
|
750
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY = "integrations_management_key_rotation_undelivered";
|
|
751
|
+
/**
|
|
752
|
+
* #398: the previous key id, still pinned in `MANAGEMENT_PUBLIC_KEYS` beside
|
|
753
|
+
* the new one, pending removal -- set the moment the new pair is pinned
|
|
754
|
+
* beside it, and cleared (to `''`, the {@link PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY}
|
|
755
|
+
* convention for "was set, now isn't") once the previous id is actually
|
|
756
|
+
* removed from the keyring. Its presence is the overlap window: both keys
|
|
757
|
+
* verify a request while it holds.
|
|
758
|
+
*/
|
|
759
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = "integrations_management_key_rotation_previous";
|
|
760
|
+
/**
|
|
761
|
+
* #398: how a rotation is requested. A person records any non-empty value
|
|
762
|
+
* under this scope key; `planIntegrations` then plans the step again and
|
|
763
|
+
* `provisionPlatformIntegrations` mints the new pair, clearing the key (to
|
|
764
|
+
* `''`) as soon as the mint is recorded, so one request is one rotation.
|
|
765
|
+
*/
|
|
766
|
+
export declare const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = "integrations_management_key_rotation_requested";
|