@wtfalch/foundry 0.59.0 → 0.61.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 (218) hide show
  1. package/dist/apps/cloudflare.d.ts +173 -0
  2. package/dist/apps/cloudflare.js +17 -0
  3. package/dist/apps/deploy.d.ts +116 -0
  4. package/dist/apps/deploy.js +791 -0
  5. package/dist/apps/github.d.ts +47 -0
  6. package/dist/apps/github.js +201 -0
  7. package/dist/apps/index.d.ts +9 -0
  8. package/dist/apps/index.js +9 -0
  9. package/dist/apps/migrations.d.ts +110 -0
  10. package/dist/apps/migrations.js +458 -0
  11. package/dist/audit/connector.d.ts +43 -0
  12. package/dist/audit/connector.js +22 -0
  13. package/dist/audit/evidence.d.ts +49 -0
  14. package/dist/audit/evidence.js +56 -0
  15. package/dist/audit/index.d.ts +9 -0
  16. package/dist/audit/index.js +9 -0
  17. package/dist/audit/migrations.d.ts +5 -0
  18. package/dist/audit/migrations.js +5 -0
  19. package/dist/audit/registry.d.ts +49 -0
  20. package/dist/audit/registry.js +44 -0
  21. package/dist/audit/seal.d.ts +49 -0
  22. package/dist/audit/seal.js +131 -0
  23. package/dist/audit/sealer-role.d.ts +33 -0
  24. package/dist/audit/sealer-role.js +56 -0
  25. package/dist/audit/signer.d.ts +40 -0
  26. package/dist/audit/signer.js +78 -0
  27. package/dist/audit/tick.d.ts +36 -0
  28. package/dist/audit/tick.js +87 -0
  29. package/dist/catalogue/copy-en.d.ts +1 -0
  30. package/dist/catalogue/copy-en.js +129 -0
  31. package/dist/catalogue/index.d.ts +9 -0
  32. package/dist/catalogue/index.js +21 -0
  33. package/dist/catalogue/integrations.d.ts +4 -0
  34. package/dist/catalogue/integrations.js +104 -0
  35. package/dist/catalogue/systems.d.ts +7 -0
  36. package/dist/catalogue/systems.js +31 -0
  37. package/dist/catalogue/types.d.ts +110 -0
  38. package/dist/catalogue/types.js +1 -0
  39. package/dist/entry-rules.d.ts +53 -0
  40. package/dist/entry-rules.js +71 -0
  41. package/dist/hosted-label.d.ts +13 -0
  42. package/dist/hosted-label.js +81 -0
  43. package/dist/index.d.ts +10 -5
  44. package/dist/index.js +5 -3
  45. package/dist/migrations/0062_hosting_github.sql +6 -0
  46. package/dist/migrations/0063_boule_apps.sql +7 -0
  47. package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
  48. package/dist/modules/ai.d.ts +3 -0
  49. package/dist/modules/ai.js +64 -0
  50. package/dist/modules/assistants.d.ts +3 -0
  51. package/dist/modules/assistants.js +72 -0
  52. package/dist/modules/audit-ledger.d.ts +29 -0
  53. package/dist/modules/audit-ledger.js +49 -0
  54. package/dist/modules/audit.d.ts +7 -0
  55. package/dist/modules/audit.js +44 -0
  56. package/dist/modules/database.d.ts +23 -0
  57. package/dist/modules/database.js +189 -0
  58. package/dist/modules/email.d.ts +3 -0
  59. package/dist/modules/email.js +18 -0
  60. package/dist/modules/entry-parts.d.ts +23 -0
  61. package/dist/modules/entry-parts.js +63 -0
  62. package/dist/modules/files.d.ts +3 -0
  63. package/dist/modules/files.js +64 -0
  64. package/dist/modules/index.d.ts +74 -0
  65. package/dist/modules/index.js +774 -0
  66. package/dist/modules/jobs.d.ts +3 -0
  67. package/dist/modules/jobs.js +23 -0
  68. package/dist/modules/organisations.d.ts +3 -0
  69. package/dist/modules/organisations.js +20 -0
  70. package/dist/modules/outputs.d.ts +9 -0
  71. package/dist/modules/outputs.js +38 -0
  72. package/dist/modules/sign-in.d.ts +3 -0
  73. package/dist/modules/sign-in.js +197 -0
  74. package/dist/modules/signed-records.d.ts +3 -0
  75. package/dist/modules/signed-records.js +32 -0
  76. package/dist/modules/types.d.ts +194 -0
  77. package/dist/modules/types.js +1 -0
  78. package/dist/modules/workspaces.d.ts +3 -0
  79. package/dist/modules/workspaces.js +75 -0
  80. package/dist/org.d.ts +163 -22
  81. package/dist/org.js +419 -47
  82. package/dist/plan/apps-db-route.d.ts +90 -0
  83. package/dist/plan/apps-db-route.js +168 -0
  84. package/dist/plan/apps-zones.d.ts +56 -0
  85. package/dist/plan/apps-zones.js +118 -0
  86. package/dist/plan/archon-blue-green.js +11 -1
  87. package/dist/plan/blue-green-apply.d.ts +37 -1
  88. package/dist/plan/blue-green-apply.js +55 -15
  89. package/dist/plan/blue-green.d.ts +41 -1
  90. package/dist/plan/blue-green.js +80 -2
  91. package/dist/plan/boule.d.ts +23 -1
  92. package/dist/plan/boule.js +35 -2
  93. package/dist/plan/broker-entries.d.ts +28 -3
  94. package/dist/plan/broker-entries.js +74 -3
  95. package/dist/plan/broker-registrar.d.ts +168 -0
  96. package/dist/plan/broker-registrar.js +440 -0
  97. package/dist/plan/broker-workspaces-database.d.ts +48 -0
  98. package/dist/plan/broker-workspaces-database.js +87 -0
  99. package/dist/plan/desktop-callback.js +12 -7
  100. package/dist/plan/diff.d.ts +8 -2
  101. package/dist/plan/diff.js +373 -42
  102. package/dist/plan/edge-access.d.ts +6 -0
  103. package/dist/plan/edge-dns.d.ts +6 -0
  104. package/dist/plan/edge-dns.js +9 -7
  105. package/dist/plan/entry-ai-key.d.ts +1 -1
  106. package/dist/plan/entry-ai-key.js +2 -2
  107. package/dist/plan/entry-audit.d.ts +58 -0
  108. package/dist/plan/entry-audit.js +122 -0
  109. package/dist/plan/entry-customer-grants.d.ts +75 -0
  110. package/dist/plan/entry-customer-grants.js +154 -0
  111. package/dist/plan/entry-edge-key.d.ts +26 -1
  112. package/dist/plan/entry-edge-key.js +42 -3
  113. package/dist/plan/entry-jobs.d.ts +64 -0
  114. package/dist/plan/entry-jobs.js +92 -0
  115. package/dist/plan/entry-repo-key-apply.d.ts +55 -0
  116. package/dist/plan/entry-repo-key-apply.js +185 -0
  117. package/dist/plan/entry-repo-key.d.ts +92 -0
  118. package/dist/plan/entry-repo-key.js +235 -0
  119. package/dist/plan/entry-workspaces.d.ts +55 -0
  120. package/dist/plan/entry-workspaces.js +90 -0
  121. package/dist/plan/executor.d.ts +191 -1
  122. package/dist/plan/executor.js +1505 -90
  123. package/dist/plan/gates.d.ts +1 -1
  124. package/dist/plan/gates.js +62 -9
  125. package/dist/plan/github-deploy-keys.d.ts +78 -0
  126. package/dist/plan/github-deploy-keys.js +305 -0
  127. package/dist/plan/github-source.d.ts +53 -0
  128. package/dist/plan/github-source.js +140 -0
  129. package/dist/plan/held-key-refs.d.ts +12 -0
  130. package/dist/plan/held-key-refs.js +12 -0
  131. package/dist/plan/hostnames.d.ts +9 -0
  132. package/dist/plan/hostnames.js +28 -1
  133. package/dist/plan/index.d.ts +9 -0
  134. package/dist/plan/index.js +6 -0
  135. package/dist/plan/jobs-scheduler-http.js +3 -0
  136. package/dist/plan/keys-registrar-http.d.ts +32 -0
  137. package/dist/plan/keys-registrar-http.js +105 -0
  138. package/dist/plan/keys-registrar.d.ts +67 -0
  139. package/dist/plan/keys-registrar.js +103 -0
  140. package/dist/plan/live.d.ts +1 -1
  141. package/dist/plan/locked-hostnames.d.ts +2 -2
  142. package/dist/plan/locked-hostnames.js +2 -2
  143. package/dist/plan/login-host.js +11 -5
  144. package/dist/plan/machine-paths.d.ts +36 -0
  145. package/dist/plan/machine-paths.js +137 -0
  146. package/dist/plan/mcp.d.ts +1 -1
  147. package/dist/plan/mcp.js +49 -17
  148. package/dist/plan/org-apps-admin.d.ts +108 -0
  149. package/dist/plan/org-apps-admin.js +434 -0
  150. package/dist/plan/org-names.js +12 -1
  151. package/dist/plan/own-diff.d.ts +23 -0
  152. package/dist/plan/own-diff.js +53 -0
  153. package/dist/plan/own-hostnames.d.ts +2 -0
  154. package/dist/plan/own-hostnames.js +1 -0
  155. package/dist/plan/own-jobs-route.d.ts +47 -0
  156. package/dist/plan/own-jobs-route.js +91 -0
  157. package/dist/plan/own-services.d.ts +4 -0
  158. package/dist/plan/own-services.js +4 -0
  159. package/dist/plan/plan-apply.d.ts +39 -0
  160. package/dist/plan/plan-apply.js +163 -9
  161. package/dist/plan/platform-diff.d.ts +2 -2
  162. package/dist/plan/platform-diff.js +7 -2
  163. package/dist/plan/platform-org.d.ts +30 -0
  164. package/dist/plan/platform-org.js +12 -1
  165. package/dist/plan/platform-providers.d.ts +2 -0
  166. package/dist/plan/platform-providers.js +7 -2
  167. package/dist/plan/post-logout-uris.js +7 -1
  168. package/dist/plan/private-pages.d.ts +26 -0
  169. package/dist/plan/private-pages.js +39 -0
  170. package/dist/plan/proxy-labels.d.ts +12 -1
  171. package/dist/plan/proxy-labels.js +32 -2
  172. package/dist/plan/release-host.d.ts +6 -2
  173. package/dist/plan/release-host.js +36 -8
  174. package/dist/plan/stage-env.d.ts +19 -1
  175. package/dist/plan/stage-env.js +25 -1
  176. package/dist/plan/stage-guards.d.ts +20 -0
  177. package/dist/plan/stage-guards.js +24 -0
  178. package/dist/plan/stage-isolation.d.ts +24 -0
  179. package/dist/plan/stage-isolation.js +29 -1
  180. package/dist/plan/worker-app-database.d.ts +47 -40
  181. package/dist/plan/worker-app-database.js +150 -152
  182. package/dist/plan/worker-app.d.ts +6 -65
  183. package/dist/plan/worker-app.js +29 -105
  184. package/dist/plan/worker.d.ts +2 -2
  185. package/dist/plan/worker.js +2 -1
  186. package/dist/plan/workspace-client.d.ts +25 -0
  187. package/dist/plan/workspace-client.js +48 -0
  188. package/dist/plan/workspace-sign-in.d.ts +55 -0
  189. package/dist/plan/workspace-sign-in.js +273 -0
  190. package/dist/plan/workspace-worker.d.ts +4 -0
  191. package/dist/plan/workspace-worker.js +13 -0
  192. package/dist/plan/workspace.d.ts +101 -0
  193. package/dist/plan/workspace.js +305 -0
  194. package/dist/release/decide.d.ts +30 -1
  195. package/dist/release/decide.js +73 -11
  196. package/dist/release/health.d.ts +8 -3
  197. package/dist/release/health.js +58 -5
  198. package/dist/release/run.d.ts +2 -5
  199. package/dist/release/run.js +4 -0
  200. package/dist/release/state.d.ts +3 -1
  201. package/dist/release/state.js +6 -0
  202. package/dist/release/types.d.ts +20 -0
  203. package/dist/requested-changes.d.ts +11 -2
  204. package/dist/requested-changes.js +11 -0
  205. package/dist/service-labels.d.ts +4 -0
  206. package/dist/service-labels.js +11 -0
  207. package/dist/slug.d.ts +2 -0
  208. package/dist/slug.js +10 -6
  209. package/dist/store/postgres.js +12 -2
  210. package/dist/store/schema.d.ts +37 -1
  211. package/dist/store/schema.js +4 -0
  212. package/dist/worker-app-config.d.ts +6 -10
  213. package/dist/worker-app-config.js +22 -17
  214. package/dist/worker-app-migrations.d.ts +105 -0
  215. package/dist/worker-app-migrations.js +166 -0
  216. package/dist/worker-app-upload.d.ts +103 -0
  217. package/dist/worker-app-upload.js +128 -0
  218. package/package.json +18 -1
@@ -25,6 +25,10 @@ export async function runRelease(input) {
25
25
  actions,
26
26
  steps: input.entry.steps,
27
27
  stepHoldMinutes: input.entry.stepHoldMinutes,
28
+ advance: input.entry.advance,
29
+ minRequests: input.entry.minRequests,
30
+ errorMargin: input.entry.errorMargin,
31
+ staffFirst: input.entry.staffFirst,
28
32
  now,
29
33
  observation: { health, ...seen },
30
34
  ...(input.hardOnly ? { hardOnly: true } : {}),
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { Recorded } from './decide.js';
2
+ import { type Recorded, resolveReleaseConfig } from './decide.js';
3
3
  import type { Color, ReleaseAction, ReleaseSignal, ReleaseState, WindowState } from './types.js';
4
4
  /**
5
5
  * The persistence the release runner and Archon share. `getScopeIds` is the
@@ -52,6 +52,8 @@ export declare function readSignal(ids: Record<string, string>, entryKey: string
52
52
  export declare function readReleaseState(store: ReleaseStore, input: {
53
53
  orgId: string;
54
54
  entryKey: string;
55
+ /** The entry's `blueGreen` as declared; defaults are resolved into `config`. */
56
+ entry?: Parameters<typeof resolveReleaseConfig>[0];
55
57
  }): Promise<ReleaseState | null>;
56
58
  declare const actionSchema: z.ZodType<ReleaseAction>;
57
59
  /** C2: ask the runner to apply an action on its next run. Archon audits it. */
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { STAGE_REF_PATTERN, resolveReleaseConfig } from './decide.js';
2
3
  /** Scope keys (C3). `entry` is a hosting entry key: never `.` or `:`. */
3
4
  export function releaseKeys(entry) {
4
5
  if (!/^[a-z0-9][a-z0-9-]*$/.test(entry))
@@ -33,6 +34,7 @@ const storedWindow = z.discriminatedUnion('kind', [
33
34
  z.object({ kind: z.literal('passed'), at: z.string() }),
34
35
  z.object({ kind: z.literal('rolled_back'), at: z.string(), reason: z.string() }),
35
36
  z.object({ kind: z.literal('cap'), since: z.string() }),
37
+ z.object({ kind: z.literal('staff'), since: z.string() }),
36
38
  ]);
37
39
  const releaseField = z
38
40
  .object({
@@ -110,6 +112,7 @@ export async function readReleaseState(store, input) {
110
112
  window: r.window,
111
113
  signal: readSignal(ids, input.entryKey),
112
114
  held: r.held,
115
+ config: resolveReleaseConfig(input.entry),
113
116
  };
114
117
  }
115
118
  const actionSchema = z.discriminatedUnion('kind', [
@@ -117,6 +120,9 @@ const actionSchema = z.discriminatedUnion('kind', [
117
120
  z.object({ kind: z.literal('rollback') }),
118
121
  z.object({ kind: z.literal('clear_held'), digest: z.string().min(1) }),
119
122
  z.object({ kind: z.literal('decide_at_cap'), decision: z.enum(['pass', 'rollback']) }),
123
+ z.object({ kind: z.literal('approve_step') }),
124
+ z.object({ kind: z.literal('open') }),
125
+ z.object({ kind: z.literal('deploy_stage'), ref: z.string().regex(STAGE_REF_PATTERN) }),
120
126
  ]);
121
127
  /** C2: ask the runner to apply an action on its next run. Archon audits it. */
122
128
  export async function requestReleaseAction(store, input) {
@@ -22,6 +22,11 @@ export type WindowState = {
22
22
  } | {
23
23
  kind: 'cap';
24
24
  since: string;
25
+ }
26
+ /** Deployed on the idle color at weight 0, waiting for an `open` action (staffFirst). */
27
+ | {
28
+ kind: 'staff';
29
+ since: string;
25
30
  };
26
31
  export interface ReleaseSignal {
27
32
  requests: number;
@@ -29,6 +34,13 @@ export interface ReleaseSignal {
29
34
  baselineRate: number | null;
30
35
  verdict: 'pass' | 'fail' | 'hard_fail' | 'inconclusive';
31
36
  }
37
+ /** The release kind of an entry, defaults resolved. */
38
+ export interface ReleaseConfig {
39
+ advance: 'time' | 'signal' | 'approval';
40
+ steps: number[];
41
+ stepHoldMinutes: number;
42
+ staffFirst: boolean;
43
+ }
32
44
  export interface ReleaseState {
33
45
  entryKey: string;
34
46
  live: Color;
@@ -38,6 +50,7 @@ export interface ReleaseState {
38
50
  window: WindowState;
39
51
  signal: ReleaseSignal | null;
40
52
  held: string[];
53
+ config: ReleaseConfig;
41
54
  }
42
55
  export type ReleaseAction = {
43
56
  kind: 'promote';
@@ -49,6 +62,13 @@ export type ReleaseAction = {
49
62
  } | {
50
63
  kind: 'decide_at_cap';
51
64
  decision: 'pass' | 'rollback';
65
+ } | {
66
+ kind: 'approve_step';
67
+ } | {
68
+ kind: 'open';
69
+ } | {
70
+ kind: 'deploy_stage';
71
+ ref: string;
52
72
  };
53
73
  export declare const COLORS: readonly Color[];
54
74
  export declare function otherColor(color: Color): Color;
@@ -16,7 +16,7 @@ import type { PlanDetail } from './plan/diff.js';
16
16
  * lowering or leaving a limit unchanged -- raising either one needs
17
17
  * approval too, until a billing field exists to say what the raise costs.
18
18
  */
19
- export type RequestableField = 'calls' | 'customers' | 'storageLimit' | 'aiLimit' | 'domain' | 'mailTier' | 'app' | 'build';
19
+ export type RequestableField = 'calls' | 'customers' | 'storageLimit' | 'aiLimit' | 'domain' | 'mailTier' | 'app' | 'build' | 'bouleApps';
20
20
  export declare const REQUESTABLE_FIELDS: readonly RequestableField[];
21
21
  /** Turn the org's own calls Matrix stack (`org.ts`'s `CallsConfig.enabled`) on or off. */
22
22
  export interface CallsChange {
@@ -75,12 +75,21 @@ export interface BuildChange {
75
75
  release: string;
76
76
  build: BuildJson;
77
77
  }
78
+ /**
79
+ * Turn `org.boule.apps` on or off (`org.ts`'s `BouleApp.apps`). The company's own Boule turns it on when it makes
80
+ * its first app, so no infra is provisioned that is not in use. On is `auto`. Off retires the edge key, both
81
+ * registrar keys and the apps database route (steps with `destroys: true`), so it always needs a person.
82
+ */
83
+ export interface BouleAppsChange {
84
+ field: 'bouleApps';
85
+ enabled: boolean;
86
+ }
78
87
  /**
79
88
  * One company-requested change to its own declared org, keyed by
80
89
  * {@link RequestableField}. `classifyChange` and the store both switch on
81
90
  * `.field`.
82
91
  */
83
- export type RequestedChangeValue = CallsChange | CustomersChange | StorageLimitChange | AiLimitChange | DomainChange | MailTierChange | AppChange | BuildChange;
92
+ export type RequestedChangeValue = CallsChange | CustomersChange | StorageLimitChange | AiLimitChange | DomainChange | MailTierChange | AppChange | BuildChange | BouleAppsChange;
84
93
  /**
85
94
  * `auto`: nothing further is required -- archon may carry the request
86
95
  * straight into a plan and apply it. `approval`: a human must approve the
@@ -8,6 +8,7 @@ export const REQUESTABLE_FIELDS = [
8
8
  'mailTier',
9
9
  'app',
10
10
  'build',
11
+ 'bouleApps',
11
12
  ];
12
13
  /**
13
14
  * Thrown by {@link classifyChange} (and by the requested-change store's
@@ -44,6 +45,8 @@ function applyRequestedChange(org, change) {
44
45
  return { ...org, domain: change.domain };
45
46
  case 'mailTier':
46
47
  return { ...org, mail: { ...org.mail, mode: change.mode } };
48
+ case 'bouleApps':
49
+ return { ...org, boule: { ...org.boule, apps: change.enabled } };
47
50
  case 'app':
48
51
  case 'build':
49
52
  return org;
@@ -123,6 +126,14 @@ export function classifyChange(org, change, plan) {
123
126
  // Archon validates the whole company for these; nothing waits for approval.
124
127
  if (change.field === 'app' || change.field === 'build')
125
128
  return 'auto';
129
+ if (change.field === 'bouleApps') {
130
+ if (typeof change.enabled !== 'boolean') {
131
+ throw new RequestedChangeError('bouleApps needs enabled, true or false');
132
+ }
133
+ // Turning on provisions only what the first app needs. Turning off retires credentials and routes
134
+ // (`destroys: true` steps), so it needs a person whether or not a plan was given.
135
+ return change.enabled ? 'auto' : 'approval';
136
+ }
126
137
  if (change.field === 'customers') {
127
138
  validateCustomersChange(change);
128
139
  // The org after the change must still pass the rule apply checks, for example open registration
@@ -0,0 +1,4 @@
1
+ /** The labels an own org's services answer on, so no hosting entry may take them. */
2
+ export declare const OWN_SERVICE_LABELS: readonly ["auth", "mail", "files", "sfu"];
3
+ /** The first label of every name foundry itself serves for a company: its own services, Agora, Boule, the cms studio, calls, the sign-in host and file shares. */
4
+ export declare const FOUNDRY_SERVICE_LABELS: ReadonlySet<string>;
@@ -0,0 +1,11 @@
1
+ /** The labels an own org's services answer on, so no hosting entry may take them. */
2
+ export const OWN_SERVICE_LABELS = ['auth', 'mail', 'files', 'sfu'];
3
+ /** The first label of every name foundry itself serves for a company: its own services, Agora, Boule, the cms studio, calls, the sign-in host and file shares. */
4
+ export const FOUNDRY_SERVICE_LABELS = new Set([
5
+ ...OWN_SERVICE_LABELS,
6
+ 'agora',
7
+ 'boule',
8
+ 'cms',
9
+ 'calls',
10
+ 'share',
11
+ ]);
package/dist/slug.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  export declare const RESERVED_SLUGS: ReadonlySet<string>;
3
3
  /** Every platform app, tunnel, project and database name starts `platform-`, so no company slug may. */
4
4
  export declare const RESERVED_SLUG_PREFIX = "platform-";
5
+ /** Whether `label` ends in a suffix reserved for another org's own hostname (`-stage`, `-pr-<n>`, `-calls`, `-agora`, `-boule`, `-auth`, `-mail`, `-files`, `-sfu`, `-cms`). */
6
+ export declare function hasBarredSlugSuffix(label: string): boolean;
5
7
  /**
6
8
  * Why {@link validateSlug} refused a candidate slug, by name so a caller —
7
9
  * `store/postgres.ts`'s `DeclaredOrgError`, eventually the operator page —
package/dist/slug.js CHANGED
@@ -151,6 +151,15 @@ const OWN_SERVICE_SUFFIX = /-(auth|mail|files|sfu|cms)$/;
151
151
  * `names-`.
152
152
  */
153
153
  const NAMES_PREFIX = /^names(-|$)/;
154
+ /** Whether `label` ends in a suffix reserved for another org's own hostname (`-stage`, `-pr-<n>`, `-calls`, `-agora`, `-boule`, `-auth`, `-mail`, `-files`, `-sfu`, `-cms`). */
155
+ export function hasBarredSlugSuffix(label) {
156
+ return (STAGE_SUFFIX.test(label) ||
157
+ PR_SUFFIX.test(label) ||
158
+ CALLS_SUFFIX.test(label) ||
159
+ AGORA_SUFFIX.test(label) ||
160
+ BOULE_SUFFIX.test(label) ||
161
+ OWN_SERVICE_SUFFIX.test(label));
162
+ }
154
163
  /**
155
164
  * Validates a candidate org slug against every rule #28 decided. Returns
156
165
  * `null` when the slug is acceptable, or a {@link SlugRejection} naming
@@ -177,12 +186,7 @@ export function validateSlug(slug) {
177
186
  message: `slug '${slug}' is 'names' or starts with 'names-', a prefix edge reserves for its own platform tunnels and refuses to create a company tunnel under (#535)`,
178
187
  };
179
188
  }
180
- if (STAGE_SUFFIX.test(slug) ||
181
- PR_SUFFIX.test(slug) ||
182
- CALLS_SUFFIX.test(slug) ||
183
- AGORA_SUFFIX.test(slug) ||
184
- BOULE_SUFFIX.test(slug) ||
185
- OWN_SERVICE_SUFFIX.test(slug)) {
189
+ if (hasBarredSlugSuffix(slug)) {
186
190
  return {
187
191
  reason: 'environment-suffix',
188
192
  message: `slug '${slug}' ends with an environment suffix ('-stage', '-pr-<digits>', '-calls', '-agora', '-boule', '-auth', '-mail', '-files', '-sfu' or '-cms') reserved for another org's own hostname (#28, #145, #164, #161) — 'acme-calls' is only legal as acme's calls hostname, 'acme-agora' only as acme's Agora hostname, 'acme-boule' only as acme's Boule hostname, never as an org slug in its own right`,
@@ -161,8 +161,15 @@ function rowToOrg(row) {
161
161
  ? { customers: { enabled: row.customersEnabled, signUp: row.customersSignUp } }
162
162
  : {}),
163
163
  // Omitted while unset. Validated on read: a stored value is not trusted.
164
- ...(row.bouleBlueGreen
165
- ? { boule: { blueGreen: readBlueGreen(row.bouleBlueGreen, 'boule') } }
164
+ ...(row.bouleBlueGreen || row.bouleApps
165
+ ? {
166
+ boule: {
167
+ ...(row.bouleBlueGreen
168
+ ? { blueGreen: readBlueGreen(row.bouleBlueGreen, 'boule') }
169
+ : {}),
170
+ ...(row.bouleApps ? { apps: true } : {}),
171
+ },
172
+ }
166
173
  : {}),
167
174
  ...(row.agoraBlueGreen
168
175
  ? { agora: { blueGreen: readBlueGreen(row.agoraBlueGreen, 'agora') } }
@@ -177,6 +184,7 @@ function rowToOrg(row) {
177
184
  entries: row.hostingEntries.map((entry) => storedEntryToEntry(entry, row.hostingUsesDomain)),
178
185
  gateLimit: row.hostingGateLimit,
179
186
  repos: row.hostingRepos,
187
+ ...(row.hostingGithub ? { github: row.hostingGithub } : {}),
180
188
  },
181
189
  sso: { enabled: row.ssoEnabled },
182
190
  guests: { enabled: row.guestsEnabled },
@@ -230,6 +238,7 @@ function orgToRow(org) {
230
238
  customersEnabled: org.customers?.enabled ?? false,
231
239
  customersSignUp: org.customers?.signUp ?? false,
232
240
  bouleBlueGreen: org.boule?.blueGreen ?? null,
241
+ bouleApps: org.boule?.apps ?? false,
233
242
  agoraBlueGreen: org.agora?.blueGreen ?? null,
234
243
  servicesOwn: org.services?.own ?? false,
235
244
  servicesSfuSlot: org.services?.sfuSlot ?? null,
@@ -243,6 +252,7 @@ function orgToRow(org) {
243
252
  hostingEntries: org.hosting.entries,
244
253
  hostingGateLimit: org.hosting.gateLimit,
245
254
  hostingRepos: normalizeHostingRepos(org.hosting.repos),
255
+ hostingGithub: org.hosting.github ?? null,
246
256
  ssoEnabled: org.sso.enabled,
247
257
  guestsEnabled: org.guests.enabled,
248
258
  soloEnabled: org.solo.enabled,
@@ -1,4 +1,4 @@
1
- import type { AppEnvSource, BlueGreenConfig, HostingEntry, IssuerApp, OwnerIdentity } from '../org.js';
1
+ import type { AppEnvSource, BlueGreenConfig, HostingEntry, HostingGithub, IssuerApp, OwnerIdentity } from '../org.js';
2
2
  import type { Plan } from '../plan.js';
3
3
  import type { Checkpoint, DestroyRecord } from '../state.js';
4
4
  /**
@@ -444,6 +444,23 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
444
444
  }, {}, {
445
445
  $type: BlueGreenConfig | null;
446
446
  }>;
447
+ bouleApps: import("drizzle-orm/pg-core").PgColumn<{
448
+ name: "boule_apps";
449
+ tableName: "foundry_declared_orgs";
450
+ dataType: "boolean";
451
+ columnType: "PgBoolean";
452
+ data: boolean;
453
+ driverParam: boolean;
454
+ notNull: true;
455
+ hasDefault: true;
456
+ isPrimaryKey: false;
457
+ isAutoincrement: false;
458
+ hasRuntimeDefault: false;
459
+ enumValues: undefined;
460
+ baseColumn: never;
461
+ identity: undefined;
462
+ generated: undefined;
463
+ }, {}, {}>;
447
464
  versionPin: import("drizzle-orm/pg-core").PgColumn<{
448
465
  name: "version_pin";
449
466
  tableName: "foundry_declared_orgs";
@@ -711,6 +728,25 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
711
728
  }, {}, {
712
729
  $type: string[];
713
730
  }>;
731
+ hostingGithub: import("drizzle-orm/pg-core").PgColumn<{
732
+ name: "hosting_github";
733
+ tableName: "foundry_declared_orgs";
734
+ dataType: "json";
735
+ columnType: "PgJsonb";
736
+ data: HostingGithub | null;
737
+ driverParam: unknown;
738
+ notNull: false;
739
+ hasDefault: false;
740
+ isPrimaryKey: false;
741
+ isAutoincrement: false;
742
+ hasRuntimeDefault: false;
743
+ enumValues: undefined;
744
+ baseColumn: never;
745
+ identity: undefined;
746
+ generated: undefined;
747
+ }, {}, {
748
+ $type: HostingGithub | null;
749
+ }>;
714
750
  ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
715
751
  name: "sso_enabled";
716
752
  tableName: "foundry_declared_orgs";
@@ -46,6 +46,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
46
46
  // OrgBuiltinApp.blueGreen (org.ts) of Boule and Agora; `migrations/0059_org_builtin_blue_green.sql`. Null is off.
47
47
  bouleBlueGreen: jsonb('boule_blue_green').$type(),
48
48
  agoraBlueGreen: jsonb('agora_blue_green').$type(),
49
+ // BouleApp.apps (org.ts): Boule runs apps for the company; `migrations/0063_boule_apps.sql`.
50
+ bouleApps: boolean('boule_apps').notNull().default(false),
49
51
  // VersionPin (org.ts): null follows the platform. `migrations/0053_org_version_pin.sql`.
50
52
  versionPin: jsonb('version_pin').$type(),
51
53
  // SiteConfig (org.ts, T10). A brand new org has no cms site.
@@ -76,6 +78,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
76
78
  hostingGateLimit: integer('hosting_gate_limit').notNull().default(3),
77
79
  // HostingConfig.repos (org.ts); migrations/0060. Lowercase `wtfalch/<name>` strings.
78
80
  hostingRepos: jsonb('hosting_repos').notNull().default([]).$type(),
81
+ // HostingConfig.github (org.ts); migrations/0062. Null: the company has no GitHub App installation.
82
+ hostingGithub: jsonb('hosting_github').$type(),
79
83
  ssoEnabled: boolean('sso_enabled').notNull().default(false),
80
84
  guestsEnabled: boolean('guests_enabled').notNull().default(false),
81
85
  soloEnabled: boolean('solo_enabled').notNull().default(false),
@@ -60,10 +60,14 @@ export interface WorkerAppConfig {
60
60
  * or truncate. `migrationsSha256` (64 lowercase hex) pins the release's
61
61
  * `migrations.json`: required when the release has one, and foundry refuses
62
62
  * to parse bytes whose sha256 differs. Omit it only for a release with no
63
- * `migrations.json`; one that has the file is then refused.
63
+ * `migrations.json`; one that has the file is then refused. `grants` names
64
+ * functions the runtime role may execute, as `name(type, ...)` or
65
+ * `schema.name(type, ...)`: the security definer functions a migration
66
+ * revoked from public, such as `@wtfalch/audit`'s ledger functions.
64
67
  */
65
68
  database?: {
66
69
  appendOnly?: string[];
70
+ grants?: string[];
67
71
  migrationsSha256?: string;
68
72
  };
69
73
  /** Plain vars. Never secrets. */
@@ -76,14 +80,8 @@ export declare const WORKER_APP_COMPATIBILITY_FLAGS: ReadonlySet<string>;
76
80
  export declare const WORKER_APP_INSTANCE_TYPES: ReadonlySet<string>;
77
81
  /** The most instances one container class may scale to. */
78
82
  export declare const WORKER_APP_MAX_INSTANCES = 100;
79
- /** The secret that carries a `keysCustody` source: the custody Worker credential. */
80
- export declare const WORKER_APP_KEYS_SECRET = "KEYS_CREDENTIAL";
83
+ export { WORKER_APP_APPEND_ONLY_MAX, WORKER_APP_GRANTS_MAX, WORKER_APP_KEYS_SECRET, WORKER_APP_RESERVED_NAMES, } from './entry-rules.js';
81
84
  export declare const WORKER_APP_KEYS_URL_VAR = "KEYS_URL";
82
- /**
83
- * Names foundry writes into a Worker app's environment itself, so a
84
- * declaration may not use them as a binding, secret or var.
85
- */
86
- export declare const WORKER_APP_RESERVED_NAMES: RegExp;
87
85
  /** True when the entry needs the org's Postgres database: a Coolify app with `database`, or a Worker app with `workerApp.database`. */
88
86
  export declare const entryHasDatabase: (entry: {
89
87
  database: boolean;
@@ -93,8 +91,6 @@ export declare const entryHasDatabase: (entry: {
93
91
  }) => boolean;
94
92
  /** The Hyperdrive binding of a Worker app with `database`. */
95
93
  export declare const WORKER_APP_HYPERDRIVE_BINDING = "HYPERDRIVE";
96
- /** The most tables `database.appendOnly` may name, and the shape of one name (`table` or `schema.table`). */
97
- export declare const WORKER_APP_APPEND_ONLY_MAX = 100;
98
94
  /** Hex characters of the org id's sha256 in a host id. */
99
95
  export declare const WORKER_APP_HOST_ID_HASH_LENGTH = 12;
100
96
  /**
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { WORKER_APP_ENV_NAME as ENV_NAME, WORKER_APP_FUNCTION_SIGNATURE as FUNCTION_SIGNATURE, SECRET_LIKE_NAME as SECRET_NAME, WORKER_APP_TABLE_NAME as TABLE_NAME, WORKER_APP_APPEND_ONLY_MAX, WORKER_APP_GRANTS_MAX, WORKER_APP_KEYS_SECRET, WORKER_APP_RESERVED_NAMES, } from './entry-rules.js';
2
3
  import { validateSlug } from './slug.js';
3
4
  /** The compatibility flags a Worker app may ask for. */
4
5
  export const WORKER_APP_COMPATIBILITY_FLAGS = new Set(['nodejs_compat']);
@@ -12,21 +13,12 @@ export const WORKER_APP_INSTANCE_TYPES = new Set([
12
13
  ]);
13
14
  /** The most instances one container class may scale to. */
14
15
  export const WORKER_APP_MAX_INSTANCES = 100;
15
- /** The secret that carries a `keysCustody` source: the custody Worker credential. */
16
- export const WORKER_APP_KEYS_SECRET = 'KEYS_CREDENTIAL';
16
+ export { WORKER_APP_APPEND_ONLY_MAX, WORKER_APP_GRANTS_MAX, WORKER_APP_KEYS_SECRET, WORKER_APP_RESERVED_NAMES, } from './entry-rules.js';
17
17
  export const WORKER_APP_KEYS_URL_VAR = 'KEYS_URL';
18
- /**
19
- * Names foundry writes into a Worker app's environment itself, so a
20
- * declaration may not use them as a binding, secret or var.
21
- */
22
- export const WORKER_APP_RESERVED_NAMES = /^(AUTH_.*|KEYS_.*|MCP_.*|HYPERDRIVE)$/;
23
18
  /** True when the entry needs the org's Postgres database: a Coolify app with `database`, or a Worker app with `workerApp.database`. */
24
19
  export const entryHasDatabase = (entry) => entry.database || Boolean(entry.workerApp?.database);
25
20
  /** The Hyperdrive binding of a Worker app with `database`. */
26
21
  export const WORKER_APP_HYPERDRIVE_BINDING = 'HYPERDRIVE';
27
- /** The most tables `database.appendOnly` may name, and the shape of one name (`table` or `schema.table`). */
28
- export const WORKER_APP_APPEND_ONLY_MAX = 100;
29
- const TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,62}(\.[A-Za-z_][A-Za-z0-9_]{0,62})?$/;
30
22
  /** The custody Worker's host id shape for a derived (app) host, and its limit. */
31
23
  const HOST_ID = /^app-[a-z0-9]+(-[a-z0-9]+)*$/;
32
24
  const HOST_ID_MAX_LENGTH = 64;
@@ -55,12 +47,10 @@ const LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
55
47
  const RELEASE_ASSET_URL = /^https:\/\/github\.com\/wtfalch\/([A-Za-z0-9._-]+)\/releases\/download\/([A-Za-z0-9._+-]+)\/([A-Za-z0-9._+-]+)$/;
56
48
  const SHA256_HEX = /^[0-9a-f]{64}$/;
57
49
  const DIGEST = /^sha256:[0-9a-f]{64}$/;
58
- const ENV_NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
59
50
  const CLASS_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
60
51
  const MIGRATION_TAG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
61
52
  const IMAGE_REPOSITORY = /^[a-z0-9]+([._-][a-z0-9]+)*(:[0-9]+)?(\/[a-z0-9]+([._-][a-z0-9]+)*)+$/;
62
53
  const COMPAT_DATE = /^\d{4}-\d{2}-\d{2}$/;
63
- const SECRET_NAME = /_(KEY|SECRET|TOKEN|PASSWORD)$/;
64
54
  const VAR_MAX_LENGTH = 1000;
65
55
  /**
66
56
  * Checks an artifact reference: an https URL with no userinfo, query or
@@ -203,9 +193,9 @@ export function validateEntryWorkerApp(entry, hosting, names) {
203
193
  if (Object.keys(entry.env).length > 0) {
204
194
  return `${at} has no Coolify app, so env does not apply: use workerApp.vars or workerApp.secrets`;
205
195
  }
206
- const unsupported = Object.keys(entry.uses ?? {}).find((use) => use !== 'auth');
196
+ const unsupported = Object.keys(entry.uses ?? {}).find((use) => use !== 'auth' && use !== 'audit');
207
197
  if (unsupported)
208
- return `${at} supports uses.auth only, not uses.${unsupported}`;
198
+ return `${at} supports uses.auth and uses.audit only, not uses.${unsupported}`;
209
199
  if (entry.key.length > WORKER_NAME_MAX_LENGTH - WORKER_NAME_FIXED_LENGTH) {
210
200
  return `${at} entry key is too long: the deployed Worker's name (an 8-character org hash, the key, and "${WORKER_APP_NAME_SUFFIX}") must fit Cloudflare's ${WORKER_NAME_MAX_LENGTH}-character limit`;
211
201
  }
@@ -342,11 +332,26 @@ function validateWorkerAppDatabase(at, app) {
342
332
  if (database === undefined)
343
333
  return null;
344
334
  if (typeof database !== 'object' || database === null || Array.isArray(database)) {
345
- return `${at} database must be an object: { appendOnly?: string[]; migrationsSha256?: string }`;
335
+ return `${at} database must be an object: { appendOnly?: string[]; grants?: string[]; migrationsSha256?: string }`;
346
336
  }
347
- const unknown = Object.keys(database).find((key) => key !== 'appendOnly' && key !== 'migrationsSha256');
337
+ const unknown = Object.keys(database).find((key) => key !== 'appendOnly' && key !== 'grants' && key !== 'migrationsSha256');
348
338
  if (unknown) {
349
- return `${at} database.${unknown} is not a setting: only appendOnly and migrationsSha256`;
339
+ return `${at} database.${unknown} is not a setting: only appendOnly, grants and migrationsSha256`;
340
+ }
341
+ const grants = database.grants;
342
+ if (grants !== undefined) {
343
+ if (!Array.isArray(grants) || grants.length > WORKER_APP_GRANTS_MAX) {
344
+ return `${at} database.grants must be a list of at most ${WORKER_APP_GRANTS_MAX} function signatures`;
345
+ }
346
+ const seenGrants = new Set();
347
+ for (const signature of grants) {
348
+ if (typeof signature !== 'string' || !FUNCTION_SIGNATURE.test(signature)) {
349
+ return `${at} database.grants names '${String(signature).slice(0, 80)}', which is not a function signature (name(type, ...) or schema.name(type, ...))`;
350
+ }
351
+ if (seenGrants.has(signature))
352
+ return `${at} database.grants names '${signature}' twice`;
353
+ seenGrants.add(signature);
354
+ }
350
355
  }
351
356
  const sha = database.migrationsSha256;
352
357
  if (sha !== undefined && (typeof sha !== 'string' || !SHA256_HEX.test(sha))) {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * A Worker app's database changes (the release's `migrations.json`), pure and shared: the
3
+ * platform apply (`plan/worker-app-database.ts`) and `@wtfalch/foundry/apps` both run them
4
+ * through `runWorkerAppMigrations` with their own SQL runner. No node beyond crypto, no network.
5
+ */
6
+ /** One migration of a release's `migrations.json`. */
7
+ export interface DatabaseMigration {
8
+ id: string;
9
+ sql: string;
10
+ }
11
+ /**
12
+ * The SQL a Worker app's database changes need, as the owner `O` of the database: the platform
13
+ * path reaches it through an Access tunnel (`HyperdriveOrigin`), a company's own Postgres by URL
14
+ * (`@wtfalch/foundry/apps`). Every method is idempotent.
15
+ */
16
+ export interface WorkerAppMigrationRunner<O> {
17
+ /** The migration ids the database's ledger holds, in the order applied. */
18
+ appliedMigrationIds(owner: O): Promise<string[]>;
19
+ /** Runs `migration.sql` and records `migration.id` in the ledger in one transaction. */
20
+ applyMigration(owner: O, migration: DatabaseMigration): Promise<void>;
21
+ /**
22
+ * Creates or updates the `runtime.user` role with `runtime.password`, grants it DML on the
23
+ * tables, revokes UPDATE, DELETE and TRUNCATE from it on each `appendOnly` table, and grants
24
+ * it EXECUTE on each `grants` function.
25
+ */
26
+ ensureRuntimeRole(input: {
27
+ owner: O;
28
+ runtime: {
29
+ user: string;
30
+ password: string;
31
+ };
32
+ appendOnly: string[];
33
+ grants: string[];
34
+ }): Promise<void>;
35
+ }
36
+ export declare const WORKER_APP_MIGRATION_TIMEOUT_MS = 300000;
37
+ export declare const MIGRATIONS_ASSET_NAME = "migrations.json";
38
+ /**
39
+ * Reads a release's `migrations.json`. `null` text is an absent file: no migrations. A file that is
40
+ * not a list of `{id, sql}` with unique ids is refused, naming no SQL.
41
+ */
42
+ export declare function parseMigrations(text: string | null): DatabaseMigration[];
43
+ /**
44
+ * A present `migrations.json` runs as the database owner, so its bytes must be the ones the
45
+ * declaration pinned. No file needs no digest; a file with no declared digest, or a different
46
+ * one, is refused before it is parsed. The message names neither the SQL nor the wanted digest.
47
+ */
48
+ export declare function verifyMigrationsDigest(text: string | null, declared: string | undefined): void;
49
+ /** The URL of `migrations.json` beside the module: the same release, another file. */
50
+ export declare const migrationsUrl: (artifactUrl: string) => string;
51
+ /**
52
+ * The migrations the database does not hold, in order. The applied ledger must be the first
53
+ * migrations of the file: a missing or reordered one is refused (migrations are append-only).
54
+ */
55
+ export declare function unappliedDatabaseMigrations(all: readonly DatabaseMigration[], applied: readonly string[]): DatabaseMigration[];
56
+ /** A migration the platform adds for a module (today `uses.audit`: the pinned `@wtfalch/audit` migrations). */
57
+ export interface PlatformMigration extends DatabaseMigration {
58
+ }
59
+ /** A platform migration not run because the app already ships it, and how that was known. */
60
+ export interface SkippedPlatformMigration {
61
+ id: string;
62
+ /** `same id` or `same sql` (a copy under another id), plain words for the log. */
63
+ why: string;
64
+ }
65
+ /**
66
+ * The platform migrations still to run, in order, and the ones skipped because the release's own list carries
67
+ * them (same id, or the same SQL under another id: Candor ships its own copy of the audit migrations).
68
+ * A platform migration the database's ledger already holds is neither: it ran before. Never applies twice.
69
+ */
70
+ export declare function pendingPlatformMigrations(platform: readonly PlatformMigration[], own: readonly DatabaseMigration[], applied: readonly string[]): {
71
+ pending: PlatformMigration[];
72
+ skipped: SkippedPlatformMigration[];
73
+ };
74
+ /**
75
+ * The build's database changes, shared by the platform apply and `@wtfalch/foundry/apps`:
76
+ * `text` (the release's `migrations.json`, `null` when it has none) is checked against
77
+ * `want.migrationsSha256`, parsed, the unapplied migrations run in order as the owner (each
78
+ * recorded by the runner's ledger), then the runtime role is made true. A failed migration
79
+ * throws; those before it stay applied. Messages start with `at` and name no SQL.
80
+ */
81
+ export declare function runWorkerAppMigrations<O>(runner: WorkerAppMigrationRunner<O>, owner: O, text: string | null, want: {
82
+ appendOnly: readonly string[];
83
+ grants?: readonly string[];
84
+ migrationsSha256?: string;
85
+ }, runtime: {
86
+ user: string;
87
+ password: string;
88
+ }, opts: {
89
+ at: string;
90
+ call: <T>(label: string, task: () => Promise<T>, ms?: number) => Promise<T>;
91
+ migrationTimeoutMs?: number;
92
+ /** After each migration, with every id applied so far. */
93
+ onApplied?: (done: string[]) => Promise<void>;
94
+ /**
95
+ * Migrations a module needs that the app need not ship (`uses.audit`). They run first, each once, unless the
96
+ * release's own list carries the same one; their ids are not part of the release's append-only check.
97
+ */
98
+ platform?: readonly PlatformMigration[];
99
+ /** One plain line per skipped platform migration. */
100
+ log?: (line: string) => void;
101
+ }): Promise<{
102
+ applied: string[];
103
+ ran: string[];
104
+ skipped: SkippedPlatformMigration[];
105
+ }>;