@wtfalch/foundry 0.4.0 → 0.6.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/bootstrap.d.ts +111 -0
- package/dist/bootstrap/bootstrap.js +323 -0
- package/dist/bootstrap/declare.d.ts +36 -0
- package/dist/bootstrap/declare.js +99 -0
- package/dist/bootstrap/drive.d.ts +40 -0
- package/dist/bootstrap/drive.js +51 -0
- package/dist/bootstrap/index.d.ts +3 -0
- package/dist/bootstrap/index.js +1 -0
- package/dist/calls-sfu-deploy-support.d.ts +17 -6
- package/dist/calls-sfu-deploy-support.js +19 -7
- package/dist/calls-sfu-keys.d.ts +21 -0
- package/dist/calls-sfu-keys.js +27 -0
- package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
- package/dist/calls-sfu-ssh-adapter.js +78 -0
- package/dist/calls-sfu-ssh-client.d.ts +49 -0
- package/dist/calls-sfu-ssh-client.js +82 -0
- package/dist/calls-sfu.d.ts +36 -12
- package/dist/calls-sfu.js +41 -9
- package/dist/index.d.ts +38 -7
- package/dist/index.js +20 -4
- package/dist/migrations/0016_platform.sql +61 -0
- package/dist/migrations/0017_requested_changes.sql +42 -0
- package/dist/migrations/0018_platform_apply.sql +19 -0
- package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
- package/dist/migrations/0020_mail_apps.sql +7 -0
- package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
- package/dist/org.d.ts +81 -0
- package/dist/org.js +96 -0
- package/dist/plan/agora.d.ts +77 -0
- package/dist/plan/agora.js +127 -0
- package/dist/plan/cloudflared-connector.d.ts +70 -0
- package/dist/plan/cloudflared-connector.js +106 -0
- package/dist/plan/diff.d.ts +20 -3
- package/dist/plan/diff.js +713 -115
- package/dist/plan/executor.d.ts +680 -172
- package/dist/plan/executor.js +755 -189
- package/dist/plan/index.d.ts +28 -5
- package/dist/plan/index.js +15 -2
- package/dist/plan/jobs-scheduler-http.d.ts +29 -0
- package/dist/plan/jobs-scheduler-http.js +50 -0
- package/dist/plan/live.d.ts +51 -2
- package/dist/plan/live.js +8 -0
- package/dist/plan/mail-copy.d.ts +125 -0
- package/dist/plan/mail-copy.js +166 -0
- package/dist/plan/mail-identity-allowlist.d.ts +50 -0
- package/dist/plan/mail-identity-allowlist.js +11 -0
- package/dist/plan/mail-identity.d.ts +68 -0
- package/dist/plan/mail-identity.js +17 -0
- package/dist/plan/plan-apply.d.ts +197 -3
- package/dist/plan/plan-apply.js +278 -9
- package/dist/plan/platform-diff.d.ts +487 -0
- package/dist/plan/platform-diff.js +1220 -0
- package/dist/plan/platform-org-ports.d.ts +50 -0
- package/dist/plan/platform-org-ports.js +70 -0
- package/dist/plan/platform-org.d.ts +121 -0
- package/dist/plan/platform-org.js +115 -0
- package/dist/plan/platform-providers.d.ts +615 -0
- package/dist/plan/platform-providers.js +4410 -0
- package/dist/plan/platform-sfu.d.ts +128 -0
- package/dist/plan/platform-sfu.js +142 -0
- package/dist/plan/providers-readers.js +22 -1
- package/dist/plan/zitadel-machine-user.d.ts +83 -0
- package/dist/plan/zitadel-machine-user.js +238 -0
- package/dist/plan.d.ts +6 -0
- package/dist/platform.d.ts +55 -0
- package/dist/platform.js +46 -0
- package/dist/requested-changes.d.ts +118 -0
- package/dist/requested-changes.js +125 -0
- package/dist/slug.js +15 -2
- package/dist/store/audit.js +4 -2
- package/dist/store/postgres.d.ts +29 -1
- package/dist/store/postgres.js +203 -4
- package/dist/store/schema.d.ts +755 -0
- package/dist/store/schema.js +70 -0
- package/dist/store.d.ts +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { OrgId } from './org.js';
|
|
2
|
+
/**
|
|
3
|
+
* A shared service the declared platform runs, one entry per key on
|
|
4
|
+
* {@link DeclaredPlatform}. `enabled` turns the service's own provisioning
|
|
5
|
+
* on; `hostname` is where it is served (default `<service>.<apex>`,
|
|
6
|
+
* {@link defaultPlatformHostname}); `gitRef` is the release it runs, the
|
|
7
|
+
* same "declared, not discovered" shape `HostingConfig.gitBranch` uses for
|
|
8
|
+
* an org's own app (`org.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export interface PlatformService {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
hostname: string;
|
|
13
|
+
gitRef: string;
|
|
14
|
+
}
|
|
15
|
+
/** The eight shared services a declared platform names, decided in #165. */
|
|
16
|
+
export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon';
|
|
17
|
+
export declare const PLATFORM_SERVICE_KEYS: readonly PlatformServiceKey[];
|
|
18
|
+
/**
|
|
19
|
+
* The one shared platform every org's services run on -- decided in #165 as
|
|
20
|
+
* one declared platform, owned by Company Included, with its own plan and
|
|
21
|
+
* apply, mirroring `DeclaredOrg` (`org.ts`) for the shape a human declares
|
|
22
|
+
* and foundry drives live state to match.
|
|
23
|
+
*
|
|
24
|
+
* `ownerOrgId` is Company Included's own {@link OrgId} -- the platform is not
|
|
25
|
+
* a level-1 org itself, but its resources are billed and owned by one. There
|
|
26
|
+
* is exactly one declared platform (`store/postgres.ts`'s
|
|
27
|
+
* `createPostgresPlatformStore`, `migrations/0016_platform.sql`'s singleton
|
|
28
|
+
* table); a second one is refused at the database level, not by convention.
|
|
29
|
+
*/
|
|
30
|
+
export interface DeclaredPlatform {
|
|
31
|
+
ownerOrgId: OrgId;
|
|
32
|
+
/** The shared apex every service defaults its hostname under, e.g. `companyincluded.com`. */
|
|
33
|
+
apex: string;
|
|
34
|
+
auth: PlatformService;
|
|
35
|
+
mail: PlatformService;
|
|
36
|
+
sfu: PlatformService;
|
|
37
|
+
files: PlatformService;
|
|
38
|
+
ai: PlatformService;
|
|
39
|
+
jobs: PlatformService;
|
|
40
|
+
integrations: PlatformService;
|
|
41
|
+
archon: PlatformService;
|
|
42
|
+
}
|
|
43
|
+
/** `<service>.<apex>` -- the default `hostname` for a platform service, before an operator overrides it. */
|
|
44
|
+
export declare function defaultPlatformHostname(service: PlatformServiceKey, apex: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* `null` when `platform` is valid; otherwise the reason it is not, same
|
|
47
|
+
* shape as `org.ts`'s `validateBuyerIdentity`.
|
|
48
|
+
*
|
|
49
|
+
* - `apex` is a valid domain (`org.ts`'s `validateDomain`).
|
|
50
|
+
* - Every service's `hostname` is the apex itself or a subdomain of it -- a
|
|
51
|
+
* platform service does not live outside the platform's own apex.
|
|
52
|
+
* - Every service's `gitRef` is non-empty once that service is `enabled` --
|
|
53
|
+
* an enabled service with nothing to deploy is not yet declared.
|
|
54
|
+
*/
|
|
55
|
+
export declare function validatePlatform(platform: DeclaredPlatform): string | null;
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { validateDomain } from './org.js';
|
|
2
|
+
export const PLATFORM_SERVICE_KEYS = [
|
|
3
|
+
'auth',
|
|
4
|
+
'mail',
|
|
5
|
+
'sfu',
|
|
6
|
+
'files',
|
|
7
|
+
'ai',
|
|
8
|
+
'jobs',
|
|
9
|
+
'integrations',
|
|
10
|
+
'archon',
|
|
11
|
+
];
|
|
12
|
+
/** `<service>.<apex>` -- the default `hostname` for a platform service, before an operator overrides it. */
|
|
13
|
+
export function defaultPlatformHostname(service, apex) {
|
|
14
|
+
return `${service}.${apex}`;
|
|
15
|
+
}
|
|
16
|
+
function serviceOf(platform, key) {
|
|
17
|
+
return platform[key];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `null` when `platform` is valid; otherwise the reason it is not, same
|
|
21
|
+
* shape as `org.ts`'s `validateBuyerIdentity`.
|
|
22
|
+
*
|
|
23
|
+
* - `apex` is a valid domain (`org.ts`'s `validateDomain`).
|
|
24
|
+
* - Every service's `hostname` is the apex itself or a subdomain of it -- a
|
|
25
|
+
* platform service does not live outside the platform's own apex.
|
|
26
|
+
* - Every service's `gitRef` is non-empty once that service is `enabled` --
|
|
27
|
+
* an enabled service with nothing to deploy is not yet declared.
|
|
28
|
+
*/
|
|
29
|
+
export function validatePlatform(platform) {
|
|
30
|
+
const apexReason = validateDomain(platform.apex);
|
|
31
|
+
if (apexReason)
|
|
32
|
+
return `apex invalid: ${apexReason}`;
|
|
33
|
+
for (const key of PLATFORM_SERVICE_KEYS) {
|
|
34
|
+
const service = serviceOf(platform, key);
|
|
35
|
+
if (!service.hostname.trim()) {
|
|
36
|
+
return `${key}.hostname must not be empty`;
|
|
37
|
+
}
|
|
38
|
+
if (service.hostname !== platform.apex && !service.hostname.endsWith(`.${platform.apex}`)) {
|
|
39
|
+
return `${key}.hostname '${service.hostname}' must be '${platform.apex}' or a subdomain of it`;
|
|
40
|
+
}
|
|
41
|
+
if (service.enabled && !service.gitRef.trim()) {
|
|
42
|
+
return `${key}.gitRef must not be empty once ${key}.enabled is true`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { DeclaredOrg, MailMode, OrgId } from './org.js';
|
|
2
|
+
import type { PlanDetail } from './plan/diff.js';
|
|
3
|
+
/**
|
|
4
|
+
* The whitelist of `DeclaredOrg` fields a company's own Boule can request a
|
|
5
|
+
* change to (#178, map #165: "a company's Boule can request setting changes
|
|
6
|
+
* on its own declared org, and Archon applies them"). Everything else is
|
|
7
|
+
* refused by {@link classifyChange} -- there is no field this module accepts
|
|
8
|
+
* outside this list.
|
|
9
|
+
*
|
|
10
|
+
* Decided in #178 "Decide": `calls`, `storageLimit` and `aiLimit` are small
|
|
11
|
+
* and safe; `domain` and `mailTier` are full self-service too, but both are
|
|
12
|
+
* migrating steps (`plan/diff.ts`'s `planMail` moves live mail between
|
|
13
|
+
* modes; a domain move is at least as disruptive) and always need approval,
|
|
14
|
+
* never auto. #184 narrows `storageLimit`/`aiLimit`'s "small and safe" to
|
|
15
|
+
* lowering or leaving a limit unchanged -- raising either one needs
|
|
16
|
+
* approval too, until a billing field exists to say what the raise costs.
|
|
17
|
+
*/
|
|
18
|
+
export type RequestableField = 'calls' | 'storageLimit' | 'aiLimit' | 'domain' | 'mailTier';
|
|
19
|
+
export declare const REQUESTABLE_FIELDS: readonly RequestableField[];
|
|
20
|
+
/** Turn the org's own calls Matrix stack (`org.ts`'s `CallsConfig.enabled`) on or off. */
|
|
21
|
+
export interface CallsChange {
|
|
22
|
+
field: 'calls';
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Change the org's own spend limit on Files (`org.ts`'s `StorageConfig`). */
|
|
26
|
+
export interface StorageLimitChange {
|
|
27
|
+
field: 'storageLimit';
|
|
28
|
+
limitBytes: string | null;
|
|
29
|
+
limitMicros: string | null;
|
|
30
|
+
}
|
|
31
|
+
/** Change the org's own spend limit on AI (`org.ts`'s `AiConfig`). */
|
|
32
|
+
export interface AiLimitChange {
|
|
33
|
+
field: 'aiLimit';
|
|
34
|
+
limitMicros: string | null;
|
|
35
|
+
}
|
|
36
|
+
/** Change the org's own domain fact (`org.ts`'s `DeclaredOrg.domain`). */
|
|
37
|
+
export interface DomainChange {
|
|
38
|
+
field: 'domain';
|
|
39
|
+
domain: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** Move the org's mail between levels (`org.ts`'s `MailConfig.mode`). */
|
|
42
|
+
export interface MailTierChange {
|
|
43
|
+
field: 'mailTier';
|
|
44
|
+
mode: MailMode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One company-requested change to its own declared org, keyed by
|
|
48
|
+
* {@link RequestableField}. `classifyChange` and the store both switch on
|
|
49
|
+
* `.field`.
|
|
50
|
+
*/
|
|
51
|
+
export type RequestedChangeValue = CallsChange | StorageLimitChange | AiLimitChange | DomainChange | MailTierChange;
|
|
52
|
+
/**
|
|
53
|
+
* `auto`: nothing further is required -- archon may carry the request
|
|
54
|
+
* straight into a plan and apply it. `approval`: a human must approve the
|
|
55
|
+
* request before archon applies it.
|
|
56
|
+
*/
|
|
57
|
+
export type ChangeClassification = 'auto' | 'approval';
|
|
58
|
+
/**
|
|
59
|
+
* A stored {@link RequestedChangeValue}'s own lifecycle. `auto` and
|
|
60
|
+
* `needs_approval` are the two states {@link classifyChange} can produce at
|
|
61
|
+
* create time -- never chosen directly; `approved`, `rejected` and
|
|
62
|
+
* `applied` are archon's own workflow carrying a `needs_approval` request
|
|
63
|
+
* onward. Foundry only stores and classifies (#178 "Why"); nothing in this
|
|
64
|
+
* package transitions a state past its initial value.
|
|
65
|
+
*/
|
|
66
|
+
export type RequestedChangeState = 'auto' | 'needs_approval' | 'approved' | 'rejected' | 'applied';
|
|
67
|
+
/**
|
|
68
|
+
* One company-requested change to its own declared org, as stored --
|
|
69
|
+
* `migrations/0017_requested_changes.sql`'s `foundry_requested_changes`
|
|
70
|
+
* row, keyed by `id` and scoped to `orgId`.
|
|
71
|
+
*/
|
|
72
|
+
export interface RequestedChange {
|
|
73
|
+
id: number;
|
|
74
|
+
orgId: OrgId;
|
|
75
|
+
value: RequestedChangeValue;
|
|
76
|
+
requestedBy: string;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
state: RequestedChangeState;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Thrown by {@link classifyChange} (and by the requested-change store's
|
|
82
|
+
* `create`, which calls it) for a `change.field` outside
|
|
83
|
+
* {@link REQUESTABLE_FIELDS} -- same "throw with a reason" shape
|
|
84
|
+
* `store/postgres.ts`'s `DeclaredOrgError` and `PlatformError` use.
|
|
85
|
+
*/
|
|
86
|
+
export declare class RequestedChangeError extends Error {
|
|
87
|
+
constructor(message: string);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `auto` when `change` is small and safe (`calls`, and `storageLimit`/
|
|
91
|
+
* `aiLimit` in the lowering or unchanged direction), the plan it causes has
|
|
92
|
+
* no destructive or migrating step, and it causes no price change on
|
|
93
|
+
* `org`'s own billing fields; `approval` otherwise -- every `domain` or
|
|
94
|
+
* `mailTier` change classifies `approval` unconditionally, since #178
|
|
95
|
+
* "Decide" calls both migrating steps that always need approval, whatever
|
|
96
|
+
* direction the change moves.
|
|
97
|
+
*
|
|
98
|
+
* A `storageLimit` or `aiLimit` change that *raises* either limit
|
|
99
|
+
* ({@link isLimitRaise} against `org`'s current value) classifies
|
|
100
|
+
* `approval`, decided in #184: "a raise of the storage or AI limit
|
|
101
|
+
* classifies `approval` until billing fields exist. A lowering stays
|
|
102
|
+
* `auto`." `storageLimit` carries two independent limits (`limitBytes`,
|
|
103
|
+
* `limitMicros`); a raise on either one is enough.
|
|
104
|
+
*
|
|
105
|
+
* `plan` is the {@link PlanDetail} `change` causes -- the caller's own diff
|
|
106
|
+
* against live reality, from the same `computePlanDetail` (`plan/diff.ts`)
|
|
107
|
+
* every other apply goes through. Any `plannedSteps` entry marked
|
|
108
|
+
* `destroys` or `migrates` classifies `approval`, whatever `change.field`
|
|
109
|
+
* says: a field-name rule only ever knows what a change usually does, not
|
|
110
|
+
* what it does to this org's actual live state today (#184 "Why"). `plan`
|
|
111
|
+
* is optional because not every caller has live state to diff against yet
|
|
112
|
+
* (`store/postgres.ts`'s `create` classifies from `org` alone); omitting it
|
|
113
|
+
* falls back to the field-name rule below, unchanged from #178.
|
|
114
|
+
*
|
|
115
|
+
* Throws {@link RequestedChangeError} for a `change.field` outside
|
|
116
|
+
* {@link REQUESTABLE_FIELDS} -- "everything else is refused" (#178 "How").
|
|
117
|
+
*/
|
|
118
|
+
export declare function classifyChange(org: DeclaredOrg, change: RequestedChangeValue, plan?: PlanDetail): ChangeClassification;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export const REQUESTABLE_FIELDS = [
|
|
2
|
+
'calls',
|
|
3
|
+
'storageLimit',
|
|
4
|
+
'aiLimit',
|
|
5
|
+
'domain',
|
|
6
|
+
'mailTier',
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Thrown by {@link classifyChange} (and by the requested-change store's
|
|
10
|
+
* `create`, which calls it) for a `change.field` outside
|
|
11
|
+
* {@link REQUESTABLE_FIELDS} -- same "throw with a reason" shape
|
|
12
|
+
* `store/postgres.ts`'s `DeclaredOrgError` and `PlatformError` use.
|
|
13
|
+
*/
|
|
14
|
+
export class RequestedChangeError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'RequestedChangeError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `org` patched by `change`, scoped to exactly the one field it names --
|
|
22
|
+
* the same `{ ...current, ...patch }` shape `store/postgres.ts`'s `update`
|
|
23
|
+
* uses for a `DeclaredOrg` patch, narrowed to a single field here since a
|
|
24
|
+
* requested change only ever touches one.
|
|
25
|
+
*/
|
|
26
|
+
function applyRequestedChange(org, change) {
|
|
27
|
+
switch (change.field) {
|
|
28
|
+
case 'calls':
|
|
29
|
+
return { ...org, calls: { ...org.calls, enabled: change.enabled } };
|
|
30
|
+
case 'storageLimit':
|
|
31
|
+
return {
|
|
32
|
+
...org,
|
|
33
|
+
storage: { limitBytes: change.limitBytes, limitMicros: change.limitMicros },
|
|
34
|
+
};
|
|
35
|
+
case 'aiLimit':
|
|
36
|
+
return { ...org, ai: { limitMicros: change.limitMicros } };
|
|
37
|
+
case 'domain':
|
|
38
|
+
return { ...org, domain: change.domain };
|
|
39
|
+
case 'mailTier':
|
|
40
|
+
return { ...org, mail: { ...org.mail, mode: change.mode } };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether `before` and `after` differ on the org's own billing fields --
|
|
45
|
+
* "take price changes from the org's billing fields" (#178 "How").
|
|
46
|
+
* `DeclaredOrg` carries no price field yet (billing itself is
|
|
47
|
+
* `@wtfalch/billing`, a separate package this one does not depend on), so
|
|
48
|
+
* this always returns `false` today. It exists as the one place
|
|
49
|
+
* `classifyChange` asks the question, so a later billing field lands here
|
|
50
|
+
* rather than reshaping `classifyChange`'s own contract.
|
|
51
|
+
*/
|
|
52
|
+
function priceChanges(_before, _after) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether a `StorageConfig`/`AiConfig` limit field moved up from `before` to
|
|
57
|
+
* `after` -- both are `null`-or-bigint-string, `null` meaning uncapped
|
|
58
|
+
* (`org.ts`'s `StorageConfig`/`AiConfig` doc). `null` is the largest
|
|
59
|
+
* possible limit, so moving a capped `before` to an uncapped `after` is a
|
|
60
|
+
* raise; moving an already-uncapped `before` anywhere is never a raise
|
|
61
|
+
* (there is nothing higher to move to); two finite values compare as
|
|
62
|
+
* bigints, never as numbers, the same bigint-safety `org.ts` requires of
|
|
63
|
+
* these fields. Equal values are not a raise.
|
|
64
|
+
*/
|
|
65
|
+
function isLimitRaise(before, after) {
|
|
66
|
+
if (before === null)
|
|
67
|
+
return false;
|
|
68
|
+
if (after === null)
|
|
69
|
+
return true;
|
|
70
|
+
return BigInt(after) > BigInt(before);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `auto` when `change` is small and safe (`calls`, and `storageLimit`/
|
|
74
|
+
* `aiLimit` in the lowering or unchanged direction), the plan it causes has
|
|
75
|
+
* no destructive or migrating step, and it causes no price change on
|
|
76
|
+
* `org`'s own billing fields; `approval` otherwise -- every `domain` or
|
|
77
|
+
* `mailTier` change classifies `approval` unconditionally, since #178
|
|
78
|
+
* "Decide" calls both migrating steps that always need approval, whatever
|
|
79
|
+
* direction the change moves.
|
|
80
|
+
*
|
|
81
|
+
* A `storageLimit` or `aiLimit` change that *raises* either limit
|
|
82
|
+
* ({@link isLimitRaise} against `org`'s current value) classifies
|
|
83
|
+
* `approval`, decided in #184: "a raise of the storage or AI limit
|
|
84
|
+
* classifies `approval` until billing fields exist. A lowering stays
|
|
85
|
+
* `auto`." `storageLimit` carries two independent limits (`limitBytes`,
|
|
86
|
+
* `limitMicros`); a raise on either one is enough.
|
|
87
|
+
*
|
|
88
|
+
* `plan` is the {@link PlanDetail} `change` causes -- the caller's own diff
|
|
89
|
+
* against live reality, from the same `computePlanDetail` (`plan/diff.ts`)
|
|
90
|
+
* every other apply goes through. Any `plannedSteps` entry marked
|
|
91
|
+
* `destroys` or `migrates` classifies `approval`, whatever `change.field`
|
|
92
|
+
* says: a field-name rule only ever knows what a change usually does, not
|
|
93
|
+
* what it does to this org's actual live state today (#184 "Why"). `plan`
|
|
94
|
+
* is optional because not every caller has live state to diff against yet
|
|
95
|
+
* (`store/postgres.ts`'s `create` classifies from `org` alone); omitting it
|
|
96
|
+
* falls back to the field-name rule below, unchanged from #178.
|
|
97
|
+
*
|
|
98
|
+
* Throws {@link RequestedChangeError} for a `change.field` outside
|
|
99
|
+
* {@link REQUESTABLE_FIELDS} -- "everything else is refused" (#178 "How").
|
|
100
|
+
*/
|
|
101
|
+
export function classifyChange(org, change, plan) {
|
|
102
|
+
if (!REQUESTABLE_FIELDS.includes(change.field)) {
|
|
103
|
+
throw new RequestedChangeError(`'${change.field}' is not a requestable field`);
|
|
104
|
+
}
|
|
105
|
+
if (plan?.plannedSteps.some((planned) => planned.step.destroys || planned.step.migrates)) {
|
|
106
|
+
return 'approval';
|
|
107
|
+
}
|
|
108
|
+
const next = applyRequestedChange(org, change);
|
|
109
|
+
if (priceChanges(org, next))
|
|
110
|
+
return 'approval';
|
|
111
|
+
switch (change.field) {
|
|
112
|
+
case 'calls':
|
|
113
|
+
return 'auto';
|
|
114
|
+
case 'storageLimit':
|
|
115
|
+
return isLimitRaise(org.storage.limitBytes, change.limitBytes) ||
|
|
116
|
+
isLimitRaise(org.storage.limitMicros, change.limitMicros)
|
|
117
|
+
? 'approval'
|
|
118
|
+
: 'auto';
|
|
119
|
+
case 'aiLimit':
|
|
120
|
+
return isLimitRaise(org.ai.limitMicros, change.limitMicros) ? 'approval' : 'auto';
|
|
121
|
+
case 'domain':
|
|
122
|
+
case 'mailTier':
|
|
123
|
+
return 'approval';
|
|
124
|
+
}
|
|
125
|
+
}
|
package/dist/slug.js
CHANGED
|
@@ -118,6 +118,16 @@ const PR_SUFFIX = /-pr-\d+$/;
|
|
|
118
118
|
const STAGE_SUFFIX = /-stage$/;
|
|
119
119
|
/** `<something>-calls` — a level-1 org's own calls service hostname (foundry#145's "Decided", 2026-09-27). */
|
|
120
120
|
const CALLS_SUFFIX = /-calls$/;
|
|
121
|
+
/**
|
|
122
|
+
* `<something>-agora` — a level-1 org's own Agora hostname
|
|
123
|
+
* (`<slug>-agora.oikos.host`, foundry#164, the 2026-09-24 layout,
|
|
124
|
+
* foundry#14). Every org's Agora is always on (#164 "How": no new field on
|
|
125
|
+
* `DeclaredOrg` to opt out of it), so this is not an opt-in reservation the
|
|
126
|
+
* way `CALLS_SUFFIX` guards a feature an org can decline -- any slug ending
|
|
127
|
+
* `-agora` collides with *every* org's own Agora hostname the moment it is
|
|
128
|
+
* taken, not just one that turns a feature on.
|
|
129
|
+
*/
|
|
130
|
+
const AGORA_SUFFIX = /-agora$/;
|
|
121
131
|
/**
|
|
122
132
|
* Validates a candidate org slug against every rule #28 decided. Returns
|
|
123
133
|
* `null` when the slug is acceptable, or a {@link SlugRejection} naming
|
|
@@ -138,10 +148,13 @@ export function validateSlug(slug) {
|
|
|
138
148
|
message: `slug '${slug}' is reserved by the estate (#28) and cannot be taken by an org`,
|
|
139
149
|
};
|
|
140
150
|
}
|
|
141
|
-
if (STAGE_SUFFIX.test(slug) ||
|
|
151
|
+
if (STAGE_SUFFIX.test(slug) ||
|
|
152
|
+
PR_SUFFIX.test(slug) ||
|
|
153
|
+
CALLS_SUFFIX.test(slug) ||
|
|
154
|
+
AGORA_SUFFIX.test(slug)) {
|
|
142
155
|
return {
|
|
143
156
|
reason: 'environment-suffix',
|
|
144
|
-
message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>' or '-
|
|
157
|
+
message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>', '-calls' or '-agora') reserved for another org's own hostname (#28, #145, #164) — 'acme-calls' is only legal as acme's calls hostname, 'acme-agora' only as acme's Agora hostname, never as an org slug in its own right`,
|
|
145
158
|
};
|
|
146
159
|
}
|
|
147
160
|
return null;
|
package/dist/store/audit.js
CHANGED
|
@@ -6,7 +6,7 @@ import { foundryAuditEvents } from './schema.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export function createPostgresAuditLog(db) {
|
|
8
8
|
return {
|
|
9
|
-
async recordPlan({ actor, orgId, plan, at }) {
|
|
9
|
+
async recordPlan({ actor, orgId, plan, target, at }) {
|
|
10
10
|
await db.insert(foundryAuditEvents).values({
|
|
11
11
|
at,
|
|
12
12
|
kind: 'plan',
|
|
@@ -14,9 +14,10 @@ export function createPostgresAuditLog(db) {
|
|
|
14
14
|
actorDisplay: actor.display,
|
|
15
15
|
orgId,
|
|
16
16
|
plan,
|
|
17
|
+
target: target ?? null,
|
|
17
18
|
});
|
|
18
19
|
},
|
|
19
|
-
async recordApply({ actor, orgId, plan, outcome, reason, at }) {
|
|
20
|
+
async recordApply({ actor, orgId, plan, outcome, reason, target, at }) {
|
|
20
21
|
await db.insert(foundryAuditEvents).values({
|
|
21
22
|
at,
|
|
22
23
|
kind: 'apply',
|
|
@@ -26,6 +27,7 @@ export function createPostgresAuditLog(db) {
|
|
|
26
27
|
outcome,
|
|
27
28
|
reason: reason ?? null,
|
|
28
29
|
plan,
|
|
30
|
+
target: target ?? null,
|
|
29
31
|
});
|
|
30
32
|
},
|
|
31
33
|
async recordDestroy({ actor, orgId, outcome, reason, at }) {
|
package/dist/store/postgres.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
|
|
2
|
-
import type { DeclaredOrgStore } from '../store.js';
|
|
2
|
+
import type { DeclaredOrgStore, PlatformStore, RequestedChangeStore } from '../store.js';
|
|
3
3
|
/**
|
|
4
4
|
* The host's own drizzle handle or a transaction open on it, same shape
|
|
5
5
|
* `@wtfalch/keys`'s `HeldKeysDb` uses: postgres-js in a real host, PGlite in
|
|
@@ -48,3 +48,31 @@ export declare class DeclaredOrgError extends Error {
|
|
|
48
48
|
* decide; `update` still merges and stores any other patch as given.
|
|
49
49
|
*/
|
|
50
50
|
export declare function createPostgresDeclaredOrgStore(db: FoundryStoreDb): DeclaredOrgStore;
|
|
51
|
+
/**
|
|
52
|
+
* Thrown by `put` when `validatePlatform` (`../platform.js`) refuses the
|
|
53
|
+
* platform given to it -- same "throw with a reason" shape
|
|
54
|
+
* {@link DeclaredOrgError} gives `create`/`update`.
|
|
55
|
+
*/
|
|
56
|
+
export declare class PlatformError extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `PlatformStore` (`../store.ts`) on Postgres, for #166: get and put the one
|
|
61
|
+
* {@link DeclaredPlatform}, beside `createPostgresDeclaredOrgStore` above.
|
|
62
|
+
* `put` validates with `../platform.js`'s `validatePlatform` and then
|
|
63
|
+
* upserts onto the fixed singleton row `migrations/0016_platform.sql`
|
|
64
|
+
* declares -- the first call creates it, every later call replaces it, and
|
|
65
|
+
* the table's own `check (id)` constraint is what actually refuses a second
|
|
66
|
+
* row, not this function.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createPostgresPlatformStore(db: FoundryStoreDb): PlatformStore;
|
|
69
|
+
/**
|
|
70
|
+
* `RequestedChangeStore` (`../store.ts`) on Postgres, for #178: classify and
|
|
71
|
+
* store a company's own requested setting change on its declared org, beside
|
|
72
|
+
* `createPostgresDeclaredOrgStore` and `createPostgresPlatformStore` above.
|
|
73
|
+
* `create` classifies with `../requested-changes.js`'s `classifyChange`
|
|
74
|
+
* against the `org` given to it -- this store never loads the org itself,
|
|
75
|
+
* the same "caller already has it" choice `PlatformStore.put`'s `platform`
|
|
76
|
+
* argument makes -- and stores the result as the request's initial `state`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createPostgresRequestedChangeStore(db: FoundryStoreDb): RequestedChangeStore;
|