@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.
Files changed (76) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +111 -0
  2. package/dist/bootstrap/bootstrap.js +323 -0
  3. package/dist/bootstrap/declare.d.ts +36 -0
  4. package/dist/bootstrap/declare.js +99 -0
  5. package/dist/bootstrap/drive.d.ts +40 -0
  6. package/dist/bootstrap/drive.js +51 -0
  7. package/dist/bootstrap/index.d.ts +3 -0
  8. package/dist/bootstrap/index.js +1 -0
  9. package/dist/calls-sfu-deploy-support.d.ts +17 -6
  10. package/dist/calls-sfu-deploy-support.js +19 -7
  11. package/dist/calls-sfu-keys.d.ts +21 -0
  12. package/dist/calls-sfu-keys.js +27 -0
  13. package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
  14. package/dist/calls-sfu-ssh-adapter.js +78 -0
  15. package/dist/calls-sfu-ssh-client.d.ts +49 -0
  16. package/dist/calls-sfu-ssh-client.js +82 -0
  17. package/dist/calls-sfu.d.ts +36 -12
  18. package/dist/calls-sfu.js +41 -9
  19. package/dist/index.d.ts +38 -7
  20. package/dist/index.js +20 -4
  21. package/dist/migrations/0016_platform.sql +61 -0
  22. package/dist/migrations/0017_requested_changes.sql +42 -0
  23. package/dist/migrations/0018_platform_apply.sql +19 -0
  24. package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
  25. package/dist/migrations/0020_mail_apps.sql +7 -0
  26. package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
  27. package/dist/org.d.ts +81 -0
  28. package/dist/org.js +96 -0
  29. package/dist/plan/agora.d.ts +77 -0
  30. package/dist/plan/agora.js +127 -0
  31. package/dist/plan/cloudflared-connector.d.ts +70 -0
  32. package/dist/plan/cloudflared-connector.js +106 -0
  33. package/dist/plan/diff.d.ts +20 -3
  34. package/dist/plan/diff.js +713 -115
  35. package/dist/plan/executor.d.ts +680 -172
  36. package/dist/plan/executor.js +755 -189
  37. package/dist/plan/index.d.ts +28 -5
  38. package/dist/plan/index.js +15 -2
  39. package/dist/plan/jobs-scheduler-http.d.ts +29 -0
  40. package/dist/plan/jobs-scheduler-http.js +50 -0
  41. package/dist/plan/live.d.ts +51 -2
  42. package/dist/plan/live.js +8 -0
  43. package/dist/plan/mail-copy.d.ts +125 -0
  44. package/dist/plan/mail-copy.js +166 -0
  45. package/dist/plan/mail-identity-allowlist.d.ts +50 -0
  46. package/dist/plan/mail-identity-allowlist.js +11 -0
  47. package/dist/plan/mail-identity.d.ts +68 -0
  48. package/dist/plan/mail-identity.js +17 -0
  49. package/dist/plan/plan-apply.d.ts +197 -3
  50. package/dist/plan/plan-apply.js +278 -9
  51. package/dist/plan/platform-diff.d.ts +487 -0
  52. package/dist/plan/platform-diff.js +1220 -0
  53. package/dist/plan/platform-org-ports.d.ts +50 -0
  54. package/dist/plan/platform-org-ports.js +70 -0
  55. package/dist/plan/platform-org.d.ts +121 -0
  56. package/dist/plan/platform-org.js +115 -0
  57. package/dist/plan/platform-providers.d.ts +615 -0
  58. package/dist/plan/platform-providers.js +4410 -0
  59. package/dist/plan/platform-sfu.d.ts +128 -0
  60. package/dist/plan/platform-sfu.js +142 -0
  61. package/dist/plan/providers-readers.js +22 -1
  62. package/dist/plan/zitadel-machine-user.d.ts +83 -0
  63. package/dist/plan/zitadel-machine-user.js +238 -0
  64. package/dist/plan.d.ts +6 -0
  65. package/dist/platform.d.ts +55 -0
  66. package/dist/platform.js +46 -0
  67. package/dist/requested-changes.d.ts +118 -0
  68. package/dist/requested-changes.js +125 -0
  69. package/dist/slug.js +15 -2
  70. package/dist/store/audit.js +4 -2
  71. package/dist/store/postgres.d.ts +29 -1
  72. package/dist/store/postgres.js +203 -4
  73. package/dist/store/schema.d.ts +755 -0
  74. package/dist/store/schema.js +70 -0
  75. package/dist/store.d.ts +53 -0
  76. 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;
@@ -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) || PR_SUFFIX.test(slug) || CALLS_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 '-calls') reserved for another org's own hostname (#28, #145) — 'acme-calls' is only legal as acme's calls hostname, never as an org slug in its own right`,
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;
@@ -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 }) {
@@ -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;