@wtfalch/foundry 0.5.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 (42) hide show
  1. package/dist/bootstrap/bootstrap.d.ts +16 -1
  2. package/dist/bootstrap/bootstrap.js +40 -2
  3. package/dist/bootstrap/declare.js +6 -1
  4. package/dist/calls-sfu-deploy-support.d.ts +8 -0
  5. package/dist/calls-sfu-deploy-support.js +10 -1
  6. package/dist/calls-sfu.d.ts +31 -8
  7. package/dist/calls-sfu.js +36 -5
  8. package/dist/index.d.ts +8 -2
  9. package/dist/index.js +5 -2
  10. package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
  11. package/dist/migrations/0020_mail_apps.sql +7 -0
  12. package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
  13. package/dist/org.d.ts +81 -0
  14. package/dist/org.js +88 -0
  15. package/dist/plan/diff.js +93 -10
  16. package/dist/plan/executor.d.ts +236 -35
  17. package/dist/plan/executor.js +274 -76
  18. package/dist/plan/index.d.ts +7 -1
  19. package/dist/plan/index.js +4 -1
  20. package/dist/plan/mail-copy.d.ts +125 -0
  21. package/dist/plan/mail-copy.js +166 -0
  22. package/dist/plan/mail-identity-allowlist.d.ts +50 -0
  23. package/dist/plan/mail-identity-allowlist.js +11 -0
  24. package/dist/plan/mail-identity.d.ts +68 -0
  25. package/dist/plan/mail-identity.js +17 -0
  26. package/dist/plan/plan-apply.d.ts +5 -0
  27. package/dist/plan/platform-diff.d.ts +184 -9
  28. package/dist/plan/platform-diff.js +368 -15
  29. package/dist/plan/platform-org.d.ts +3 -2
  30. package/dist/plan/platform-providers.d.ts +138 -24
  31. package/dist/plan/platform-providers.js +1460 -79
  32. package/dist/plan/platform-sfu.d.ts +54 -19
  33. package/dist/plan/platform-sfu.js +64 -22
  34. package/dist/plan/zitadel-machine-user.d.ts +60 -11
  35. package/dist/plan/zitadel-machine-user.js +182 -39
  36. package/dist/requested-changes.d.ts +17 -7
  37. package/dist/requested-changes.js +37 -7
  38. package/dist/store/postgres.js +31 -2
  39. package/dist/store/schema.d.ts +72 -0
  40. package/dist/store/schema.js +13 -0
  41. package/dist/store.d.ts +7 -0
  42. package/package.json +1 -1
@@ -53,12 +53,37 @@ function priceChanges(_before, _after) {
53
53
  return false;
54
54
  }
55
55
  /**
56
- * `auto` when `change` is small and safe (`calls`, `storageLimit`,
57
- * `aiLimit`), the plan it causes has no destructive or migrating step, and
58
- * it causes no price change on `org`'s own billing fields; `approval`
59
- * otherwise -- every `domain` or `mailTier` change classifies `approval`
60
- * unconditionally, since #178 "Decide" calls both migrating steps that
61
- * always need approval, whatever direction the change moves.
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.
62
87
  *
63
88
  * `plan` is the {@link PlanDetail} `change` causes -- the caller's own diff
64
89
  * against live reality, from the same `computePlanDetail` (`plan/diff.ts`)
@@ -85,9 +110,14 @@ export function classifyChange(org, change, plan) {
85
110
  return 'approval';
86
111
  switch (change.field) {
87
112
  case 'calls':
113
+ return 'auto';
88
114
  case 'storageLimit':
115
+ return isLimitRaise(org.storage.limitBytes, change.limitBytes) ||
116
+ isLimitRaise(org.storage.limitMicros, change.limitMicros)
117
+ ? 'approval'
118
+ : 'auto';
89
119
  case 'aiLimit':
90
- return 'auto';
120
+ return isLimitRaise(org.ai.limitMicros, change.limitMicros) ? 'approval' : 'auto';
91
121
  case 'domain':
92
122
  case 'mailTier':
93
123
  return 'approval';
@@ -1,5 +1,5 @@
1
1
  import { asc, eq, sql } from 'drizzle-orm';
2
- import { validateBuyerIdentity, validateDomain, validateHosting, validateOwner, validateSite, } from '../org.js';
2
+ import { DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateDomain, validateHosting, validateMail, validateOwner, validateSite, } from '../org.js';
3
3
  import { validatePlatform } from '../platform.js';
4
4
  import { RequestedChangeError, classifyChange } from '../requested-changes.js';
5
5
  import { validateSlug } from '../slug.js';
@@ -59,7 +59,14 @@ function rowToOrg(row) {
59
59
  slug: row.slug,
60
60
  displayName: row.displayName,
61
61
  domain: row.domain,
62
- mail: { mode: row.mailMode, usesDomain: row.mailUsesDomain },
62
+ mail: {
63
+ mode: row.mailMode,
64
+ usesDomain: row.mailUsesDomain,
65
+ ownerMailboxAddress: row.ownerMailboxAddress,
66
+ noReplyLocalPart: row.noReplyLocalPart,
67
+ mailApps: row.mailApps,
68
+ roleMailboxes: row.roleMailboxes,
69
+ },
63
70
  chat: { usesDomain: row.chatUsesDomain },
64
71
  calls: { enabled: row.callsEnabled, usesDomain: row.callsUsesDomain },
65
72
  issuer: { usesDomain: row.issuerUsesDomain },
@@ -96,6 +103,10 @@ function orgToRow(org) {
96
103
  domain: org.domain,
97
104
  mailMode: org.mail.mode,
98
105
  mailUsesDomain: org.mail.usesDomain,
106
+ ownerMailboxAddress: org.mail.ownerMailboxAddress ?? null,
107
+ noReplyLocalPart: org.mail.noReplyLocalPart ?? DEFAULT_NO_REPLY_LOCAL_PART,
108
+ mailApps: org.mail.mailApps ?? [...DEFAULT_MAIL_APPS],
109
+ roleMailboxes: org.mail.roleMailboxes ?? [],
99
110
  chatUsesDomain: org.chat.usesDomain,
100
111
  callsEnabled: org.calls.enabled,
101
112
  callsUsesDomain: org.calls.usesDomain,
@@ -256,6 +267,16 @@ export function createPostgresDeclaredOrgStore(db) {
256
267
  if (reason)
257
268
  throw new DeclaredOrgError(`owner invalid: ${reason}`);
258
269
  }
270
+ // #162: same place `requireValidOwner` checks the owner, on both `create`
271
+ // and the merged result of `update`. Deliberately does not require
272
+ // `mail.ownerMailboxAddress` just because mail is on -- `validateMail`'s
273
+ // own doc comment says why. Also covers #34's `mailApps` check, folded
274
+ // into the same `validateMail`.
275
+ function requireValidMail(org) {
276
+ const reason = validateMail(org);
277
+ if (reason)
278
+ throw new DeclaredOrgError(`mail invalid: ${reason} (#162)`);
279
+ }
259
280
  return {
260
281
  async create(org) {
261
282
  requireValidSlug(org.slug);
@@ -265,6 +286,7 @@ export function createPostgresDeclaredOrgStore(db) {
265
286
  requireValidHosting(org);
266
287
  requireValidSite(org);
267
288
  requireValidOwner(org);
289
+ requireValidMail(org);
268
290
  try {
269
291
  await db.insert(declaredOrgs).values(orgToRow(org));
270
292
  }
@@ -289,6 +311,12 @@ export function createPostgresDeclaredOrgStore(db) {
289
311
  const [row] = await db.select().from(declaredOrgs).where(eq(declaredOrgs.id, id)).limit(1);
290
312
  return row ? rowToOrg(row) : null;
291
313
  },
314
+ // #133: ordered by display name, the same `asc(...)` shape
315
+ // `createPostgresRequestedChangeStore.listForOrg` already uses below.
316
+ async list() {
317
+ const rows = await db.select().from(declaredOrgs).orderBy(asc(declaredOrgs.displayName));
318
+ return rows.map(rowToOrg);
319
+ },
292
320
  async slugTaken(slug) {
293
321
  const [row] = await db
294
322
  .select({ id: declaredOrgs.id })
@@ -338,6 +366,7 @@ export function createPostgresDeclaredOrgStore(db) {
338
366
  requireValidHosting(next);
339
367
  requireValidSite(next);
340
368
  requireValidOwner(next);
369
+ requireValidMail(next);
341
370
  try {
342
371
  await db
343
372
  .update(declaredOrgs)
@@ -113,6 +113,78 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
113
113
  identity: undefined;
114
114
  generated: undefined;
115
115
  }, {}, {}>;
116
+ ownerMailboxAddress: import("drizzle-orm/pg-core").PgColumn<{
117
+ name: "owner_mailbox_address";
118
+ tableName: "foundry_declared_orgs";
119
+ dataType: "string";
120
+ columnType: "PgText";
121
+ data: string;
122
+ driverParam: string;
123
+ notNull: false;
124
+ hasDefault: false;
125
+ isPrimaryKey: false;
126
+ isAutoincrement: false;
127
+ hasRuntimeDefault: false;
128
+ enumValues: [string, ...string[]];
129
+ baseColumn: never;
130
+ identity: undefined;
131
+ generated: undefined;
132
+ }, {}, {}>;
133
+ noReplyLocalPart: import("drizzle-orm/pg-core").PgColumn<{
134
+ name: "no_reply_local_part";
135
+ tableName: "foundry_declared_orgs";
136
+ dataType: "string";
137
+ columnType: "PgText";
138
+ data: string;
139
+ driverParam: string;
140
+ notNull: true;
141
+ hasDefault: true;
142
+ isPrimaryKey: false;
143
+ isAutoincrement: false;
144
+ hasRuntimeDefault: false;
145
+ enumValues: [string, ...string[]];
146
+ baseColumn: never;
147
+ identity: undefined;
148
+ generated: undefined;
149
+ }, {}, {}>;
150
+ mailApps: import("drizzle-orm/pg-core").PgColumn<{
151
+ name: "mail_apps";
152
+ tableName: "foundry_declared_orgs";
153
+ dataType: "json";
154
+ columnType: "PgJsonb";
155
+ data: string[];
156
+ driverParam: unknown;
157
+ notNull: true;
158
+ hasDefault: true;
159
+ isPrimaryKey: false;
160
+ isAutoincrement: false;
161
+ hasRuntimeDefault: false;
162
+ enumValues: undefined;
163
+ baseColumn: never;
164
+ identity: undefined;
165
+ generated: undefined;
166
+ }, {}, {
167
+ $type: string[];
168
+ }>;
169
+ roleMailboxes: import("drizzle-orm/pg-core").PgColumn<{
170
+ name: "role_mailboxes";
171
+ tableName: "foundry_declared_orgs";
172
+ dataType: "json";
173
+ columnType: "PgJsonb";
174
+ data: string[];
175
+ driverParam: unknown;
176
+ notNull: true;
177
+ hasDefault: true;
178
+ isPrimaryKey: false;
179
+ isAutoincrement: false;
180
+ hasRuntimeDefault: false;
181
+ enumValues: undefined;
182
+ baseColumn: never;
183
+ identity: undefined;
184
+ generated: undefined;
185
+ }, {}, {
186
+ $type: string[];
187
+ }>;
116
188
  chatUsesDomain: import("drizzle-orm/pg-core").PgColumn<{
117
189
  name: "chat_uses_domain";
118
190
  tableName: "foundry_declared_orgs";
@@ -12,6 +12,19 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
12
12
  domain: text('domain'),
13
13
  mailMode: text('mail_mode').notNull().default('off'),
14
14
  mailUsesDomain: boolean('mail_uses_domain').notNull().default(false),
15
+ // MailConfig (org.ts, #162). null until an operator declares one -- the
16
+ // mailbox itself is blocked, not guessed, until it is.
17
+ ownerMailboxAddress: text('owner_mailbox_address'),
18
+ // Defaults to 'no-reply', the same literal `0019_mail_owner_mailbox.sql`
19
+ // and org.ts's `DEFAULT_NO_REPLY_LOCAL_PART` use.
20
+ noReplyLocalPart: text('no_reply_local_part').notNull().default('no-reply'),
21
+ // MailConfig.mailApps (org.ts, #34). Which ZITADEL apps in the org's
22
+ // audience project may read a person's mail; defaults to DEFAULT_MAIL_APPS
23
+ // ('agora', 'webmail') so every existing org keeps its current behaviour.
24
+ mailApps: jsonb('mail_apps').notNull().default(['agora', 'webmail']).$type(),
25
+ // MailConfig.roleMailboxes (org.ts, #230). Local parts of the role mailboxes
26
+ // pre-created on the org's mail domain; empty by default.
27
+ roleMailboxes: jsonb('role_mailboxes').notNull().default([]).$type(),
15
28
  chatUsesDomain: boolean('chat_uses_domain').notNull().default(false),
16
29
  callsEnabled: boolean('calls_enabled').notNull().default(false),
17
30
  callsUsesDomain: boolean('calls_uses_domain').notNull().default(false),
package/dist/store.d.ts CHANGED
@@ -14,6 +14,13 @@ import type { AppliedState } from './state.js';
14
14
  export interface DeclaredOrgStore {
15
15
  create(org: DeclaredOrg): Promise<void>;
16
16
  get(id: OrgId): Promise<DeclaredOrg | null>;
17
+ /**
18
+ * Every declared org, ordered by display name (#133). The read path a
19
+ * billing admin area (or anything else that just wants the roster) uses
20
+ * instead of reading the store's own table by hand -- Archon's
21
+ * `listCustomers` was doing exactly that before this existed.
22
+ */
23
+ list(): Promise<DeclaredOrg[]>;
17
24
  /** Whether any org already holds this slug. `slugFromName` asks it. */
18
25
  slugTaken(slug: string): Promise<boolean>;
19
26
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",