@wtfalch/foundry 0.2.0 → 0.4.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/calls-sfu-deploy-support.d.ts +57 -0
- package/dist/calls-sfu-deploy-support.js +82 -0
- package/dist/calls-sfu.d.ts +192 -0
- package/dist/calls-sfu.js +256 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -4
- package/dist/migrate.d.ts +38 -0
- package/dist/migrate.js +47 -0
- package/dist/migrations/0012_audit.sql +21 -0
- package/dist/migrations/0013_access_emails.sql +6 -0
- package/dist/migrations/0014_calls_enabled.sql +7 -0
- package/dist/migrations/0015_domain_unique.sql +39 -0
- package/dist/org.d.ts +41 -2
- package/dist/org.js +85 -0
- package/dist/plan/calls-matrix.d.ts +135 -0
- package/dist/plan/calls-matrix.js +275 -0
- package/dist/plan/diff.d.ts +8 -4
- package/dist/plan/diff.js +239 -9
- package/dist/plan/executor.d.ts +751 -14
- package/dist/plan/executor.js +1538 -32
- package/dist/plan/index.d.ts +3 -3
- package/dist/plan/index.js +2 -2
- package/dist/plan/plan-apply.js +29 -7
- package/dist/plan/providers-readers.d.ts +54 -1
- package/dist/plan/providers-readers.js +153 -1
- package/dist/provision.d.ts +56 -0
- package/dist/provision.js +69 -0
- package/dist/slug.d.ts +12 -18
- package/dist/slug.js +74 -2
- package/dist/store/audit.d.ts +8 -0
- package/dist/store/audit.js +43 -0
- package/dist/store/postgres.d.ts +11 -6
- package/dist/store/postgres.js +104 -27
- package/dist/store/schema.d.ts +205 -0
- package/dist/store/schema.js +15 -1
- package/dist/store.d.ts +12 -0
- package/package.json +9 -3
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { foundryAuditEvents } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* foundry's own {@link AuditLog}, one row per plan, apply or destroy in
|
|
4
|
+
* `foundry_audit_events` (#85). The CLI uses it before any archon exists; an
|
|
5
|
+
* attached archon reads the same table. It only inserts.
|
|
6
|
+
*/
|
|
7
|
+
export function createPostgresAuditLog(db) {
|
|
8
|
+
return {
|
|
9
|
+
async recordPlan({ actor, orgId, plan, at }) {
|
|
10
|
+
await db.insert(foundryAuditEvents).values({
|
|
11
|
+
at,
|
|
12
|
+
kind: 'plan',
|
|
13
|
+
actorId: actor.id,
|
|
14
|
+
actorDisplay: actor.display,
|
|
15
|
+
orgId,
|
|
16
|
+
plan,
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
async recordApply({ actor, orgId, plan, outcome, reason, at }) {
|
|
20
|
+
await db.insert(foundryAuditEvents).values({
|
|
21
|
+
at,
|
|
22
|
+
kind: 'apply',
|
|
23
|
+
actorId: actor.id,
|
|
24
|
+
actorDisplay: actor.display,
|
|
25
|
+
orgId,
|
|
26
|
+
outcome,
|
|
27
|
+
reason: reason ?? null,
|
|
28
|
+
plan,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
async recordDestroy({ actor, orgId, outcome, reason, at }) {
|
|
32
|
+
await db.insert(foundryAuditEvents).values({
|
|
33
|
+
at,
|
|
34
|
+
kind: 'destroy',
|
|
35
|
+
actorId: actor.id,
|
|
36
|
+
actorDisplay: actor.display,
|
|
37
|
+
orgId,
|
|
38
|
+
outcome,
|
|
39
|
+
reason: reason ?? null,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/store/postgres.d.ts
CHANGED
|
@@ -14,7 +14,10 @@ export type FoundryStoreDb = PgDatabase<PgQueryResultHKT, any, any>;
|
|
|
14
14
|
* environment-suffix shape), a slug already taken by another org (#28:
|
|
15
15
|
* checked by the unique index `0004_slug_unique.sql` adds, whether caught
|
|
16
16
|
* ahead of time by a pre-check or by a race this store maps back to the
|
|
17
|
-
* same error)
|
|
17
|
+
* same error), a domain that fails `../org.js`'s `validateDomain` or is
|
|
18
|
+
* already taken by another org (#131, the same shape as the slug: format
|
|
19
|
+
* checked ahead of time, uniqueness checked by `0015_domain_unique.sql`'s
|
|
20
|
+
* index) or — the one impossible combination #18 implements — a slug
|
|
18
21
|
* change once the org has mail. `store.ts`'s `update` doc requires a caller
|
|
19
22
|
* to be "ready for this to throw rather than silently drop a field"; this
|
|
20
23
|
* is that throw.
|
|
@@ -36,10 +39,12 @@ export declare class DeclaredOrgError extends Error {
|
|
|
36
39
|
* (or `null` for uncapped), checked the same way on both `create` and the
|
|
37
40
|
* merged result of `update`. #67 adds a fourth (`org.ts`'s
|
|
38
41
|
* `validateBuyerIdentity`) and #71 a fifth (`org.ts`'s `validateHosting`),
|
|
39
|
-
* checked the same way.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* checked the same way. #131 adds a sixth, in the same shape as the slug: the
|
|
43
|
+
* domain, when set, is a validated (`org.ts`'s `validateDomain`), globally
|
|
44
|
+
* unique DNS name, checked on both `create` and `update`. The broader
|
|
45
|
+
* "combinations foundry knows are impossible are rejected with a reason"
|
|
46
|
+
* line needs a ruleset (domain requirements per service, `adoptedApp.form`,
|
|
47
|
+
* mail-level transitions…) that later tickets, not #18, #27, #28 or #131,
|
|
48
|
+
* decide; `update` still merges and stores any other patch as given.
|
|
44
49
|
*/
|
|
45
50
|
export declare function createPostgresDeclaredOrgStore(db: FoundryStoreDb): DeclaredOrgStore;
|
package/dist/store/postgres.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import { eq, sql } from 'drizzle-orm';
|
|
2
|
-
import { validateBuyerIdentity, validateHosting, validateOwner } from '../org.js';
|
|
2
|
+
import { validateBuyerIdentity, validateDomain, validateHosting, validateOwner, validateSite, } from '../org.js';
|
|
3
3
|
import { validateSlug } from '../slug.js';
|
|
4
4
|
import { declaredOrgs, foundryAppliedState, } from './schema.js';
|
|
5
5
|
const UNIQUE_VIOLATION = '23505';
|
|
6
|
+
/** `migrations/0004_slug_unique.sql` / `migrations/0015_domain_unique.sql`'s index names, read back off a caught unique-violation to tell which one fired. */
|
|
7
|
+
const SLUG_UNIQUE_INDEX = 'foundry_declared_orgs_slug_idx';
|
|
8
|
+
const DOMAIN_UNIQUE_INDEX = 'foundry_declared_orgs_domain_idx';
|
|
6
9
|
/** #27: a bigint-safe decimal string of a non-negative integer -- no sign, no decimal point, no leading zeros (so "010" never diffs against a live "10"). */
|
|
7
10
|
const NON_NEGATIVE_INTEGER = /^(0|[1-9]\d*)$/;
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
10
|
-
* directly (PGlite in this package's own tests) or wrapped in
|
|
11
|
-
* own `DrizzleQueryError`, whose `cause` carries the raw driver
|
|
12
|
-
* (postgres-js in a host)
|
|
13
|
-
*
|
|
12
|
+
* The Postgres unique-violation constraint name, however the driver hands the
|
|
13
|
+
* error back -- directly (PGlite in this package's own tests) or wrapped in
|
|
14
|
+
* drizzle-orm's own `DrizzleQueryError`, whose `cause` carries the raw driver
|
|
15
|
+
* error (postgres-js in a host) -- or `undefined` when this is not a unique
|
|
16
|
+
* violation. Checked by Postgres error code, never by message text, since
|
|
17
|
+
* message text is not a contract. Same code-check pattern as
|
|
14
18
|
* `../runner/runner.ts`'s own `isUniqueViolation` -- kept as a local copy
|
|
15
|
-
* here rather than shared, since neither module depends on the other
|
|
19
|
+
* here rather than shared, since neither module depends on the other; this
|
|
20
|
+
* one additionally reads `constraint` so `create`/`update` can tell a slug
|
|
21
|
+
* collision from a domain collision (#131) rather than reporting both the
|
|
22
|
+
* same way.
|
|
16
23
|
*/
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
if (code === UNIQUE_VIOLATION)
|
|
20
|
-
return
|
|
24
|
+
function uniqueViolationConstraint(error) {
|
|
25
|
+
const direct = error;
|
|
26
|
+
if (direct?.code === UNIQUE_VIOLATION)
|
|
27
|
+
return direct.constraint;
|
|
21
28
|
const cause = error?.cause;
|
|
22
|
-
|
|
29
|
+
if (cause?.code === UNIQUE_VIOLATION)
|
|
30
|
+
return cause.constraint;
|
|
31
|
+
return undefined;
|
|
23
32
|
}
|
|
24
33
|
/**
|
|
25
34
|
* Thrown by `create` and `update` when the store refuses a write: an id
|
|
@@ -28,7 +37,10 @@ function isUniqueViolation(error) {
|
|
|
28
37
|
* environment-suffix shape), a slug already taken by another org (#28:
|
|
29
38
|
* checked by the unique index `0004_slug_unique.sql` adds, whether caught
|
|
30
39
|
* ahead of time by a pre-check or by a race this store maps back to the
|
|
31
|
-
* same error)
|
|
40
|
+
* same error), a domain that fails `../org.js`'s `validateDomain` or is
|
|
41
|
+
* already taken by another org (#131, the same shape as the slug: format
|
|
42
|
+
* checked ahead of time, uniqueness checked by `0015_domain_unique.sql`'s
|
|
43
|
+
* index) or — the one impossible combination #18 implements — a slug
|
|
32
44
|
* change once the org has mail. `store.ts`'s `update` doc requires a caller
|
|
33
45
|
* to be "ready for this to throw rather than silently drop a field"; this
|
|
34
46
|
* is that throw.
|
|
@@ -47,7 +59,7 @@ function rowToOrg(row) {
|
|
|
47
59
|
domain: row.domain,
|
|
48
60
|
mail: { mode: row.mailMode, usesDomain: row.mailUsesDomain },
|
|
49
61
|
chat: { usesDomain: row.chatUsesDomain },
|
|
50
|
-
calls: { usesDomain: row.callsUsesDomain },
|
|
62
|
+
calls: { enabled: row.callsEnabled, usesDomain: row.callsUsesDomain },
|
|
51
63
|
issuer: { usesDomain: row.issuerUsesDomain },
|
|
52
64
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
53
65
|
hosting: {
|
|
@@ -58,6 +70,7 @@ function rowToOrg(row) {
|
|
|
58
70
|
gitBranch: row.hostingGitBranch,
|
|
59
71
|
buildPack: row.hostingBuildPack,
|
|
60
72
|
issuerApps: row.hostingIssuerApps,
|
|
73
|
+
accessEmails: row.hostingAccessEmails,
|
|
61
74
|
},
|
|
62
75
|
sso: { enabled: row.ssoEnabled },
|
|
63
76
|
guests: { enabled: row.guestsEnabled },
|
|
@@ -82,6 +95,7 @@ function orgToRow(org) {
|
|
|
82
95
|
mailMode: org.mail.mode,
|
|
83
96
|
mailUsesDomain: org.mail.usesDomain,
|
|
84
97
|
chatUsesDomain: org.chat.usesDomain,
|
|
98
|
+
callsEnabled: org.calls.enabled,
|
|
85
99
|
callsUsesDomain: org.calls.usesDomain,
|
|
86
100
|
issuerUsesDomain: org.issuer.usesDomain,
|
|
87
101
|
siteEnabled: org.site.enabled,
|
|
@@ -93,6 +107,7 @@ function orgToRow(org) {
|
|
|
93
107
|
hostingGitBranch: org.hosting.gitBranch,
|
|
94
108
|
hostingBuildPack: org.hosting.buildPack,
|
|
95
109
|
hostingIssuerApps: org.hosting.issuerApps,
|
|
110
|
+
hostingAccessEmails: org.hosting.accessEmails,
|
|
96
111
|
ssoEnabled: org.sso.enabled,
|
|
97
112
|
guestsEnabled: org.guests.enabled,
|
|
98
113
|
soloEnabled: org.solo.enabled,
|
|
@@ -164,11 +179,13 @@ function rowToOwner(row) {
|
|
|
164
179
|
* (or `null` for uncapped), checked the same way on both `create` and the
|
|
165
180
|
* merged result of `update`. #67 adds a fourth (`org.ts`'s
|
|
166
181
|
* `validateBuyerIdentity`) and #71 a fifth (`org.ts`'s `validateHosting`),
|
|
167
|
-
* checked the same way.
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
182
|
+
* checked the same way. #131 adds a sixth, in the same shape as the slug: the
|
|
183
|
+
* domain, when set, is a validated (`org.ts`'s `validateDomain`), globally
|
|
184
|
+
* unique DNS name, checked on both `create` and `update`. The broader
|
|
185
|
+
* "combinations foundry knows are impossible are rejected with a reason"
|
|
186
|
+
* line needs a ruleset (domain requirements per service, `adoptedApp.form`,
|
|
187
|
+
* mail-level transitions…) that later tickets, not #18, #27, #28 or #131,
|
|
188
|
+
* decide; `update` still merges and stores any other patch as given.
|
|
172
189
|
*/
|
|
173
190
|
export function createPostgresDeclaredOrgStore(db) {
|
|
174
191
|
async function requireRow(id) {
|
|
@@ -182,6 +199,18 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
182
199
|
if (rejection)
|
|
183
200
|
throw new DeclaredOrgError(rejection.message);
|
|
184
201
|
}
|
|
202
|
+
// #131: same place `requireValidSlug` checks the slug's format (#28) --
|
|
203
|
+
// `domain` is nullable (a brand new org has none), so this is only called
|
|
204
|
+
// once it is set. Uniqueness itself is not pre-checked here, the same
|
|
205
|
+
// choice `requireValidSlug` makes for the slug: the unique index
|
|
206
|
+
// `0015_domain_unique.sql` adds is the source of truth, caught below.
|
|
207
|
+
function requireValidDomain(domain) {
|
|
208
|
+
if (domain === null)
|
|
209
|
+
return;
|
|
210
|
+
const reason = validateDomain(domain);
|
|
211
|
+
if (reason)
|
|
212
|
+
throw new DeclaredOrgError(`domain invalid: ${reason} (#131)`);
|
|
213
|
+
}
|
|
185
214
|
function requireValidLimit(fieldName, value) {
|
|
186
215
|
if (value !== null && !NON_NEGATIVE_INTEGER.test(value)) {
|
|
187
216
|
throw new DeclaredOrgError(`${fieldName} '${value}' must be a bigint-safe decimal string of a non-negative integer, or null for uncapped (#27)`);
|
|
@@ -211,6 +240,13 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
211
240
|
if (reason)
|
|
212
241
|
throw new DeclaredOrgError(`hosting invalid: ${reason} (#71)`);
|
|
213
242
|
}
|
|
243
|
+
// #101: same place `requireValidHosting` checks hosting, on both `create`
|
|
244
|
+
// and the merged result of `update`.
|
|
245
|
+
function requireValidSite(org) {
|
|
246
|
+
const reason = validateSite(org);
|
|
247
|
+
if (reason)
|
|
248
|
+
throw new DeclaredOrgError(`site invalid: ${reason} (#101)`);
|
|
249
|
+
}
|
|
214
250
|
// Same place `requireValidHosting` checks hosting, on both `create` and
|
|
215
251
|
// the merged result of `update`.
|
|
216
252
|
function requireValidOwner(org) {
|
|
@@ -221,21 +257,29 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
221
257
|
return {
|
|
222
258
|
async create(org) {
|
|
223
259
|
requireValidSlug(org.slug);
|
|
260
|
+
requireValidDomain(org.domain);
|
|
224
261
|
requireValidLimits(org);
|
|
225
262
|
requireValidBuyer(org);
|
|
226
263
|
requireValidHosting(org);
|
|
264
|
+
requireValidSite(org);
|
|
227
265
|
requireValidOwner(org);
|
|
228
266
|
try {
|
|
229
267
|
await db.insert(declaredOrgs).values(orgToRow(org));
|
|
230
268
|
}
|
|
231
269
|
catch (error) {
|
|
232
|
-
// The pre-
|
|
233
|
-
// taken (nothing does, on a fresh insert) -- this catches
|
|
234
|
-
// the
|
|
235
|
-
// caught by `0004_slug_unique.sql`'s
|
|
236
|
-
// through the same error a pre-check
|
|
237
|
-
|
|
270
|
+
// The pre-checks above already refuse a slug or domain this process
|
|
271
|
+
// knows is taken (nothing does, on a fresh insert) -- this catches
|
|
272
|
+
// the race they cannot: two concurrent creates for the same slug or
|
|
273
|
+
// domain, caught by `0004_slug_unique.sql`/`0015_domain_unique.sql`'s
|
|
274
|
+
// unique indexes and reported through the same error a pre-check
|
|
275
|
+
// would have thrown (#28, #131).
|
|
276
|
+
const constraint = uniqueViolationConstraint(error);
|
|
277
|
+
if (constraint === DOMAIN_UNIQUE_INDEX) {
|
|
278
|
+
throw new DeclaredOrgError(`domain '${org.domain}' is already taken by another org (#131)`);
|
|
279
|
+
}
|
|
280
|
+
if (constraint === SLUG_UNIQUE_INDEX || constraint !== undefined) {
|
|
238
281
|
throw new DeclaredOrgError(`slug '${org.slug}' is already taken by another org`);
|
|
282
|
+
}
|
|
239
283
|
throw error;
|
|
240
284
|
}
|
|
241
285
|
},
|
|
@@ -243,6 +287,31 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
243
287
|
const [row] = await db.select().from(declaredOrgs).where(eq(declaredOrgs.id, id)).limit(1);
|
|
244
288
|
return row ? rowToOrg(row) : null;
|
|
245
289
|
},
|
|
290
|
+
async slugTaken(slug) {
|
|
291
|
+
const [row] = await db
|
|
292
|
+
.select({ id: declaredOrgs.id })
|
|
293
|
+
.from(declaredOrgs)
|
|
294
|
+
.where(eq(declaredOrgs.slug, slug))
|
|
295
|
+
.limit(1);
|
|
296
|
+
return row !== undefined;
|
|
297
|
+
},
|
|
298
|
+
// #131: a plain scan, not an indexed lookup -- `serviceIds` is jsonb
|
|
299
|
+
// keyed by resource, not by provider id, and foundry's own org count is
|
|
300
|
+
// small enough that this never needs to be more than that (same
|
|
301
|
+
// reasoning `getAppliedState`/`putAppliedState` already lean on: one row
|
|
302
|
+
// per org).
|
|
303
|
+
async findOrgIdForZone(zoneId) {
|
|
304
|
+
const rows = await db
|
|
305
|
+
.select({ orgId: foundryAppliedState.orgId, serviceIds: foundryAppliedState.serviceIds })
|
|
306
|
+
.from(foundryAppliedState);
|
|
307
|
+
for (const row of rows) {
|
|
308
|
+
for (const [key, id] of Object.entries(row.serviceIds)) {
|
|
309
|
+
if (id === zoneId && key.startsWith('cloudflare:zone:'))
|
|
310
|
+
return row.orgId;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
},
|
|
246
315
|
async update(id, patch) {
|
|
247
316
|
const current = rowToOrg(await requireRow(id));
|
|
248
317
|
// The slug freeze (#7 corrections 3): once mail exists, the mail
|
|
@@ -261,9 +330,11 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
261
330
|
if (patch.slug !== undefined && patch.slug !== current.slug)
|
|
262
331
|
requireValidSlug(patch.slug);
|
|
263
332
|
const next = { ...current, ...patch, id: current.id };
|
|
333
|
+
requireValidDomain(next.domain);
|
|
264
334
|
requireValidLimits(next);
|
|
265
335
|
requireValidBuyer(next);
|
|
266
336
|
requireValidHosting(next);
|
|
337
|
+
requireValidSite(next);
|
|
267
338
|
requireValidOwner(next);
|
|
268
339
|
try {
|
|
269
340
|
await db
|
|
@@ -273,9 +344,15 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
273
344
|
}
|
|
274
345
|
catch (error) {
|
|
275
346
|
// Same race as `create`: another org's concurrent write took this
|
|
276
|
-
// slug between the pre-check above and this statement
|
|
277
|
-
|
|
347
|
+
// slug or domain between the pre-check above and this statement
|
|
348
|
+
// (#28, #131).
|
|
349
|
+
const constraint = uniqueViolationConstraint(error);
|
|
350
|
+
if (constraint === DOMAIN_UNIQUE_INDEX) {
|
|
351
|
+
throw new DeclaredOrgError(`domain '${next.domain}' is already taken by another org (#131)`);
|
|
352
|
+
}
|
|
353
|
+
if (constraint === SLUG_UNIQUE_INDEX || constraint !== undefined) {
|
|
278
354
|
throw new DeclaredOrgError(`slug '${next.slug}' is already taken by another org`);
|
|
355
|
+
}
|
|
279
356
|
throw error;
|
|
280
357
|
}
|
|
281
358
|
return next;
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IssuerApp } from '../org.js';
|
|
2
|
+
import type { Plan } from '../plan.js';
|
|
2
3
|
import type { Checkpoint } from '../state.js';
|
|
3
4
|
/**
|
|
4
5
|
* Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
|
|
@@ -129,6 +130,23 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
129
130
|
identity: undefined;
|
|
130
131
|
generated: undefined;
|
|
131
132
|
}, {}, {}>;
|
|
133
|
+
callsEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
134
|
+
name: "calls_enabled";
|
|
135
|
+
tableName: "foundry_declared_orgs";
|
|
136
|
+
dataType: "boolean";
|
|
137
|
+
columnType: "PgBoolean";
|
|
138
|
+
data: boolean;
|
|
139
|
+
driverParam: boolean;
|
|
140
|
+
notNull: true;
|
|
141
|
+
hasDefault: true;
|
|
142
|
+
isPrimaryKey: false;
|
|
143
|
+
isAutoincrement: false;
|
|
144
|
+
hasRuntimeDefault: false;
|
|
145
|
+
enumValues: undefined;
|
|
146
|
+
baseColumn: never;
|
|
147
|
+
identity: undefined;
|
|
148
|
+
generated: undefined;
|
|
149
|
+
}, {}, {}>;
|
|
132
150
|
callsUsesDomain: import("drizzle-orm/pg-core").PgColumn<{
|
|
133
151
|
name: "calls_uses_domain";
|
|
134
152
|
tableName: "foundry_declared_orgs";
|
|
@@ -318,6 +336,25 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
318
336
|
}, {}, {
|
|
319
337
|
$type: IssuerApp[];
|
|
320
338
|
}>;
|
|
339
|
+
hostingAccessEmails: import("drizzle-orm/pg-core").PgColumn<{
|
|
340
|
+
name: "hosting_access_emails";
|
|
341
|
+
tableName: "foundry_declared_orgs";
|
|
342
|
+
dataType: "json";
|
|
343
|
+
columnType: "PgJsonb";
|
|
344
|
+
data: string[];
|
|
345
|
+
driverParam: unknown;
|
|
346
|
+
notNull: true;
|
|
347
|
+
hasDefault: true;
|
|
348
|
+
isPrimaryKey: false;
|
|
349
|
+
isAutoincrement: false;
|
|
350
|
+
hasRuntimeDefault: false;
|
|
351
|
+
enumValues: undefined;
|
|
352
|
+
baseColumn: never;
|
|
353
|
+
identity: undefined;
|
|
354
|
+
generated: undefined;
|
|
355
|
+
}, {}, {
|
|
356
|
+
$type: string[];
|
|
357
|
+
}>;
|
|
321
358
|
ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
322
359
|
name: "sso_enabled";
|
|
323
360
|
tableName: "foundry_declared_orgs";
|
|
@@ -799,7 +836,175 @@ export declare const foundryAppliedState: import("drizzle-orm/pg-core").PgTableW
|
|
|
799
836
|
};
|
|
800
837
|
dialect: "pg";
|
|
801
838
|
}>;
|
|
839
|
+
/** Mirrors `migrations/0012_audit.sql`, the source of truth (#85). */
|
|
840
|
+
export declare const foundryAuditEvents: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
841
|
+
name: "foundry_audit_events";
|
|
842
|
+
schema: undefined;
|
|
843
|
+
columns: {
|
|
844
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
845
|
+
name: "id";
|
|
846
|
+
tableName: "foundry_audit_events";
|
|
847
|
+
dataType: "number";
|
|
848
|
+
columnType: "PgBigSerial53";
|
|
849
|
+
data: number;
|
|
850
|
+
driverParam: number;
|
|
851
|
+
notNull: true;
|
|
852
|
+
hasDefault: true;
|
|
853
|
+
isPrimaryKey: true;
|
|
854
|
+
isAutoincrement: false;
|
|
855
|
+
hasRuntimeDefault: false;
|
|
856
|
+
enumValues: undefined;
|
|
857
|
+
baseColumn: never;
|
|
858
|
+
identity: undefined;
|
|
859
|
+
generated: undefined;
|
|
860
|
+
}, {}, {}>;
|
|
861
|
+
at: import("drizzle-orm/pg-core").PgColumn<{
|
|
862
|
+
name: "at";
|
|
863
|
+
tableName: "foundry_audit_events";
|
|
864
|
+
dataType: "date";
|
|
865
|
+
columnType: "PgTimestamp";
|
|
866
|
+
data: Date;
|
|
867
|
+
driverParam: string;
|
|
868
|
+
notNull: true;
|
|
869
|
+
hasDefault: false;
|
|
870
|
+
isPrimaryKey: false;
|
|
871
|
+
isAutoincrement: false;
|
|
872
|
+
hasRuntimeDefault: false;
|
|
873
|
+
enumValues: undefined;
|
|
874
|
+
baseColumn: never;
|
|
875
|
+
identity: undefined;
|
|
876
|
+
generated: undefined;
|
|
877
|
+
}, {}, {}>;
|
|
878
|
+
kind: import("drizzle-orm/pg-core").PgColumn<{
|
|
879
|
+
name: "kind";
|
|
880
|
+
tableName: "foundry_audit_events";
|
|
881
|
+
dataType: "string";
|
|
882
|
+
columnType: "PgText";
|
|
883
|
+
data: "plan" | "apply" | "destroy";
|
|
884
|
+
driverParam: string;
|
|
885
|
+
notNull: true;
|
|
886
|
+
hasDefault: false;
|
|
887
|
+
isPrimaryKey: false;
|
|
888
|
+
isAutoincrement: false;
|
|
889
|
+
hasRuntimeDefault: false;
|
|
890
|
+
enumValues: [string, ...string[]];
|
|
891
|
+
baseColumn: never;
|
|
892
|
+
identity: undefined;
|
|
893
|
+
generated: undefined;
|
|
894
|
+
}, {}, {
|
|
895
|
+
$type: "plan" | "apply" | "destroy";
|
|
896
|
+
}>;
|
|
897
|
+
actorId: import("drizzle-orm/pg-core").PgColumn<{
|
|
898
|
+
name: "actor_id";
|
|
899
|
+
tableName: "foundry_audit_events";
|
|
900
|
+
dataType: "string";
|
|
901
|
+
columnType: "PgText";
|
|
902
|
+
data: string;
|
|
903
|
+
driverParam: string;
|
|
904
|
+
notNull: true;
|
|
905
|
+
hasDefault: false;
|
|
906
|
+
isPrimaryKey: false;
|
|
907
|
+
isAutoincrement: false;
|
|
908
|
+
hasRuntimeDefault: false;
|
|
909
|
+
enumValues: [string, ...string[]];
|
|
910
|
+
baseColumn: never;
|
|
911
|
+
identity: undefined;
|
|
912
|
+
generated: undefined;
|
|
913
|
+
}, {}, {}>;
|
|
914
|
+
actorDisplay: import("drizzle-orm/pg-core").PgColumn<{
|
|
915
|
+
name: "actor_display";
|
|
916
|
+
tableName: "foundry_audit_events";
|
|
917
|
+
dataType: "string";
|
|
918
|
+
columnType: "PgText";
|
|
919
|
+
data: string;
|
|
920
|
+
driverParam: string;
|
|
921
|
+
notNull: true;
|
|
922
|
+
hasDefault: false;
|
|
923
|
+
isPrimaryKey: false;
|
|
924
|
+
isAutoincrement: false;
|
|
925
|
+
hasRuntimeDefault: false;
|
|
926
|
+
enumValues: [string, ...string[]];
|
|
927
|
+
baseColumn: never;
|
|
928
|
+
identity: undefined;
|
|
929
|
+
generated: undefined;
|
|
930
|
+
}, {}, {}>;
|
|
931
|
+
orgId: import("drizzle-orm/pg-core").PgColumn<{
|
|
932
|
+
name: "org_id";
|
|
933
|
+
tableName: "foundry_audit_events";
|
|
934
|
+
dataType: "string";
|
|
935
|
+
columnType: "PgText";
|
|
936
|
+
data: string;
|
|
937
|
+
driverParam: string;
|
|
938
|
+
notNull: true;
|
|
939
|
+
hasDefault: false;
|
|
940
|
+
isPrimaryKey: false;
|
|
941
|
+
isAutoincrement: false;
|
|
942
|
+
hasRuntimeDefault: false;
|
|
943
|
+
enumValues: [string, ...string[]];
|
|
944
|
+
baseColumn: never;
|
|
945
|
+
identity: undefined;
|
|
946
|
+
generated: undefined;
|
|
947
|
+
}, {}, {}>;
|
|
948
|
+
outcome: import("drizzle-orm/pg-core").PgColumn<{
|
|
949
|
+
name: "outcome";
|
|
950
|
+
tableName: "foundry_audit_events";
|
|
951
|
+
dataType: "string";
|
|
952
|
+
columnType: "PgText";
|
|
953
|
+
data: "applied" | "refused" | "failed" | "destroyed";
|
|
954
|
+
driverParam: string;
|
|
955
|
+
notNull: false;
|
|
956
|
+
hasDefault: false;
|
|
957
|
+
isPrimaryKey: false;
|
|
958
|
+
isAutoincrement: false;
|
|
959
|
+
hasRuntimeDefault: false;
|
|
960
|
+
enumValues: [string, ...string[]];
|
|
961
|
+
baseColumn: never;
|
|
962
|
+
identity: undefined;
|
|
963
|
+
generated: undefined;
|
|
964
|
+
}, {}, {
|
|
965
|
+
$type: "applied" | "refused" | "failed" | "destroyed";
|
|
966
|
+
}>;
|
|
967
|
+
reason: import("drizzle-orm/pg-core").PgColumn<{
|
|
968
|
+
name: "reason";
|
|
969
|
+
tableName: "foundry_audit_events";
|
|
970
|
+
dataType: "string";
|
|
971
|
+
columnType: "PgText";
|
|
972
|
+
data: string;
|
|
973
|
+
driverParam: string;
|
|
974
|
+
notNull: false;
|
|
975
|
+
hasDefault: false;
|
|
976
|
+
isPrimaryKey: false;
|
|
977
|
+
isAutoincrement: false;
|
|
978
|
+
hasRuntimeDefault: false;
|
|
979
|
+
enumValues: [string, ...string[]];
|
|
980
|
+
baseColumn: never;
|
|
981
|
+
identity: undefined;
|
|
982
|
+
generated: undefined;
|
|
983
|
+
}, {}, {}>;
|
|
984
|
+
plan: import("drizzle-orm/pg-core").PgColumn<{
|
|
985
|
+
name: "plan";
|
|
986
|
+
tableName: "foundry_audit_events";
|
|
987
|
+
dataType: "json";
|
|
988
|
+
columnType: "PgJsonb";
|
|
989
|
+
data: Plan;
|
|
990
|
+
driverParam: unknown;
|
|
991
|
+
notNull: false;
|
|
992
|
+
hasDefault: false;
|
|
993
|
+
isPrimaryKey: false;
|
|
994
|
+
isAutoincrement: false;
|
|
995
|
+
hasRuntimeDefault: false;
|
|
996
|
+
enumValues: undefined;
|
|
997
|
+
baseColumn: never;
|
|
998
|
+
identity: undefined;
|
|
999
|
+
generated: undefined;
|
|
1000
|
+
}, {}, {
|
|
1001
|
+
$type: Plan;
|
|
1002
|
+
}>;
|
|
1003
|
+
};
|
|
1004
|
+
dialect: "pg";
|
|
1005
|
+
}>;
|
|
802
1006
|
export type DeclaredOrgRow = typeof declaredOrgs.$inferSelect;
|
|
803
1007
|
export type NewDeclaredOrgRow = typeof declaredOrgs.$inferInsert;
|
|
804
1008
|
export type AppliedStateRow = typeof foundryAppliedState.$inferSelect;
|
|
805
1009
|
export type NewAppliedStateRow = typeof foundryAppliedState.$inferInsert;
|
|
1010
|
+
export type AuditEventRow = typeof foundryAuditEvents.$inferSelect;
|
package/dist/store/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boolean, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { bigserial, boolean, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
2
2
|
/**
|
|
3
3
|
* Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
|
|
4
4
|
* the SQL carries the CHECKs drizzle-kit does not generate here (this
|
|
@@ -13,6 +13,7 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
13
13
|
mailMode: text('mail_mode').notNull().default('off'),
|
|
14
14
|
mailUsesDomain: boolean('mail_uses_domain').notNull().default(false),
|
|
15
15
|
chatUsesDomain: boolean('chat_uses_domain').notNull().default(false),
|
|
16
|
+
callsEnabled: boolean('calls_enabled').notNull().default(false),
|
|
16
17
|
callsUsesDomain: boolean('calls_uses_domain').notNull().default(false),
|
|
17
18
|
issuerUsesDomain: boolean('issuer_uses_domain').notNull().default(false),
|
|
18
19
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|
|
@@ -26,6 +27,7 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
26
27
|
hostingGitBranch: text('hosting_git_branch').notNull().default('main'),
|
|
27
28
|
hostingBuildPack: text('hosting_build_pack').notNull().default('nixpacks'),
|
|
28
29
|
hostingIssuerApps: jsonb('hosting_issuer_apps').notNull().default([]).$type(),
|
|
30
|
+
hostingAccessEmails: jsonb('hosting_access_emails').notNull().default([]).$type(),
|
|
29
31
|
ssoEnabled: boolean('sso_enabled').notNull().default(false),
|
|
30
32
|
guestsEnabled: boolean('guests_enabled').notNull().default(false),
|
|
31
33
|
soloEnabled: boolean('solo_enabled').notNull().default(false),
|
|
@@ -75,3 +77,15 @@ export const foundryAppliedState = pgTable('foundry_applied_state', {
|
|
|
75
77
|
lastAppliedAt: timestamp('last_applied_at', { withTimezone: true }),
|
|
76
78
|
lastAppliedDeclaredOrgVersion: text('last_applied_declared_org_version'),
|
|
77
79
|
});
|
|
80
|
+
/** Mirrors `migrations/0012_audit.sql`, the source of truth (#85). */
|
|
81
|
+
export const foundryAuditEvents = pgTable('foundry_audit_events', {
|
|
82
|
+
id: bigserial('id', { mode: 'number' }).primaryKey(),
|
|
83
|
+
at: timestamp('at', { withTimezone: true }).notNull(),
|
|
84
|
+
kind: text('kind').notNull().$type(),
|
|
85
|
+
actorId: text('actor_id').notNull(),
|
|
86
|
+
actorDisplay: text('actor_display').notNull(),
|
|
87
|
+
orgId: text('org_id').notNull(),
|
|
88
|
+
outcome: text('outcome').$type(),
|
|
89
|
+
reason: text('reason'),
|
|
90
|
+
plan: jsonb('plan').$type(),
|
|
91
|
+
});
|
package/dist/store.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ import type { AppliedState } from './state.js';
|
|
|
12
12
|
export interface DeclaredOrgStore {
|
|
13
13
|
create(org: DeclaredOrg): Promise<void>;
|
|
14
14
|
get(id: OrgId): Promise<DeclaredOrg | null>;
|
|
15
|
+
/** Whether any org already holds this slug. `slugFromName` asks it. */
|
|
16
|
+
slugTaken(slug: string): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* The org id, if any, whose applied state already records `zoneId` under a
|
|
19
|
+
* `cloudflare:zone:*` key (#131). `plan/executor.ts`'s `cloudflareZone`
|
|
20
|
+
* asks this before adopting a zone `findZoneByApex` finds, the same
|
|
21
|
+
* foreign-resource refusal `cloudflareAccess` makes by reading Cloudflare's
|
|
22
|
+
* own Access-app name -- a Cloudflare zone carries no org-identifying
|
|
23
|
+
* field of its own for that live check, so this asks the store's own
|
|
24
|
+
* record instead.
|
|
25
|
+
*/
|
|
26
|
+
findOrgIdForZone(zoneId: string): Promise<OrgId | null>;
|
|
15
27
|
/**
|
|
16
28
|
* Combinations foundry knows are impossible are rejected with a reason
|
|
17
29
|
* (#18 "Done when"), so a caller must be ready for this to throw rather
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,13 +28,19 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/migrations && find src/migrations -maxdepth 1 -name '*.sql' -exec cp {} dist/migrations/ \\;",
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
|
31
|
-
"test": "vitest run"
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"db:provision": "node scripts/foundry-db-provision.mjs",
|
|
33
|
+
"db:migrate": "node scripts/foundry-db-migrate.mjs",
|
|
34
|
+
"calls-sfu:deploy": "node scripts/calls-sfu-deploy.mjs"
|
|
32
35
|
},
|
|
33
36
|
"peerDependencies": {
|
|
34
37
|
"drizzle-orm": ">=0.45.2"
|
|
35
38
|
},
|
|
36
39
|
"dependencies": {
|
|
37
|
-
"@wtfalch/
|
|
40
|
+
"@wtfalch/auth": "0.16.0",
|
|
41
|
+
"@wtfalch/email": "0.15.0",
|
|
42
|
+
"@wtfalch/providers": "0.6.0",
|
|
43
|
+
"ssh2": "1.17.0"
|
|
38
44
|
},
|
|
39
45
|
"devDependencies": {
|
|
40
46
|
"@electric-sql/pglite": "^0.5.8",
|