@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
@@ -0,0 +1,3 @@
1
+ import type { AppModule } from './types.js';
2
+ /** `uses.jobs`: the app is a project of its own in the jobs scheduler. */
3
+ export declare const jobs: AppModule;
@@ -0,0 +1,23 @@
1
+ import { setUse } from './entry-parts.js';
2
+ /** `uses.jobs`: the app is a project of its own in the jobs scheduler. */
3
+ export const jobs = {
4
+ view: {
5
+ id: 'jobs',
6
+ name: 'Jobs in the background',
7
+ line: 'The app runs tasks on a timer or later, such as a nightly report or a reminder.',
8
+ category: 'In the background',
9
+ icon: 'clock',
10
+ runtimes: ['server'],
11
+ requires: [],
12
+ availability: 'ready',
13
+ fields: [],
14
+ adds: [
15
+ {
16
+ name: 'A scheduler',
17
+ hint: 'Calls the app when a task is due, and tries again if it fails.',
18
+ },
19
+ ],
20
+ },
21
+ toEntry: (entry, values) => setUse(entry, 'jobs', values === null ? undefined : true),
22
+ fromEntry: (entry) => (entry.uses?.jobs === true ? {} : null),
23
+ };
@@ -0,0 +1,3 @@
1
+ import type { AppModule } from './types.js';
2
+ /** Coming soon: businesses sign in with their own company accounts. No fields, nothing in the entry yet. */
3
+ export declare const organisations: AppModule;
@@ -0,0 +1,20 @@
1
+ import { clone, problem } from './entry-parts.js';
2
+ /** Coming soon: businesses sign in with their own company accounts. No fields, nothing in the entry yet. */
3
+ export const organisations = {
4
+ view: {
5
+ id: 'organisations',
6
+ name: 'Sign-in for organisations',
7
+ line: 'Businesses that use the app sign in with their own company accounts and manage their own people.',
8
+ category: 'People',
9
+ icon: 'building',
10
+ runtimes: ['server', 'worker'],
11
+ requires: ['sign-in'],
12
+ availability: 'soon',
13
+ fields: [],
14
+ adds: [],
15
+ },
16
+ toEntry: (entry, values) => values === null
17
+ ? clone(entry)
18
+ : problem(null, 'Sign-in for organisations is coming soon. It cannot be turned on yet.'),
19
+ fromEntry: () => null,
20
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The outputs a manifest `reference` may name. Kept apart from the integration list so manifest parsing can
3
+ * check references without importing the modules back (the integration list reads the modules).
4
+ */
5
+ export declare const OUTPUTS: Record<string, readonly string[]>;
6
+ /** Secret outputs: a reference to one must say `secret: true`. */
7
+ export declare const SECRET_OUTPUTS: ReadonlySet<string>;
8
+ /** Why a reference `<owner>.<output>` is refused, or null when the owner has that output. */
9
+ export declare function referenceProblem(to: string): string | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The outputs a manifest `reference` may name. Kept apart from the integration list so manifest parsing can
3
+ * check references without importing the modules back (the integration list reads the modules).
4
+ */
5
+ export const OUTPUTS = {
6
+ 'sign-in': ['issuer', 'client-id'],
7
+ assistants: ['mcp-url'],
8
+ workspaces: [],
9
+ database: ['address', 'owner-address'],
10
+ 'signed-records': ['registrar-url'],
11
+ files: ['address'],
12
+ email: ['from'],
13
+ jobs: ['url'],
14
+ ai: ['address'],
15
+ organisations: [],
16
+ audit: [],
17
+ app: ['url', 'host', 'name'],
18
+ company: ['id', 'name', 'slug'],
19
+ };
20
+ /** Secret outputs: a reference to one must say `secret: true`. */
21
+ export const SECRET_OUTPUTS = new Set([
22
+ 'database.address',
23
+ 'database.owner-address',
24
+ ]);
25
+ /** Why a reference `<owner>.<output>` is refused, or null when the owner has that output. */
26
+ export function referenceProblem(to) {
27
+ const dot = to.indexOf('.');
28
+ const owner = to.slice(0, dot);
29
+ const output = to.slice(dot + 1);
30
+ const known = Object.hasOwn(OUTPUTS, owner) ? OUTPUTS[owner] : undefined;
31
+ if (!known)
32
+ return null; // an owner that is not in the catalogue is the caller's to refuse
33
+ if (known.includes(output))
34
+ return null;
35
+ return known.length === 0
36
+ ? `"${owner}" gives the app nothing to read.`
37
+ : `"${owner}" has no "${output}". It gives: ${known.join(', ')}.`;
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { AppModule } from './types.js';
2
+ /** `uses.auth`: who signs in, whether they sign themselves up, how, and the words of the sign-in page. */
3
+ export declare const signIn: AppModule;
@@ -0,0 +1,197 @@
1
+ import { ROOM_STEPS_MAX, ROOM_STEPS_MIN, ROOM_TEXT_MAX, ROOM_UNSAFE_TEXT } from '../entry-rules.js';
2
+ import { defined, omit, problem, setUse, text, withUses, withWorkerApp } from './entry-parts.js';
3
+ /** The sign-in window's text fields: module key -> `uses.auth.room` key. */
4
+ const ROOM_TEXT_KEYS = {
5
+ roomName: 'name',
6
+ roomHeadline: 'headline',
7
+ roomLine: 'line',
8
+ roomTitle: 'title',
9
+ roomDescription: 'description',
10
+ };
11
+ const ROOM_NEEDED = ['roomName', 'roomHeadline', 'roomLine'];
12
+ const forCustomers = { key: 'users', equals: 'customers' };
13
+ const roomField = (key, label, help, kind = 'text') => ({
14
+ key,
15
+ label,
16
+ help,
17
+ kind,
18
+ advanced: true,
19
+ max: ROOM_TEXT_MAX[ROOM_TEXT_KEYS[key]],
20
+ showWhen: forCustomers,
21
+ });
22
+ /** `uses.auth`: who signs in, whether they sign themselves up, how, and the words of the sign-in page. */
23
+ export const signIn = {
24
+ view: {
25
+ id: 'sign-in',
26
+ name: 'Sign-in',
27
+ line: 'People sign in to the app with their own account.',
28
+ category: 'People',
29
+ icon: 'key',
30
+ runtimes: ['server', 'worker'],
31
+ requires: [],
32
+ availability: 'ready',
33
+ fields: [
34
+ {
35
+ key: 'users',
36
+ label: 'Who signs in',
37
+ kind: 'choice',
38
+ required: true,
39
+ default: 'customers',
40
+ choices: [
41
+ {
42
+ value: 'customers',
43
+ label: 'Your customers',
44
+ help: 'People outside your company who use the app.',
45
+ },
46
+ {
47
+ value: 'staff',
48
+ label: 'Your team',
49
+ help: 'Only the people in your company.',
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ key: 'registration',
55
+ label: 'Who can make an account',
56
+ kind: 'choice',
57
+ required: true,
58
+ default: 'open',
59
+ choices: [
60
+ {
61
+ value: 'open',
62
+ label: 'Anyone',
63
+ help: 'People make their own account on the sign-in page.',
64
+ },
65
+ {
66
+ value: 'closed',
67
+ label: 'Only people you invite',
68
+ },
69
+ ],
70
+ },
71
+ {
72
+ key: 'method',
73
+ label: 'How people sign in',
74
+ kind: 'choice',
75
+ required: true,
76
+ default: 'password',
77
+ choices: [
78
+ { value: 'email-code', label: 'A code sent by email', help: 'No password to remember.' },
79
+ { value: 'password', label: 'A password' },
80
+ { value: 'passkey', label: 'A passkey', soon: true },
81
+ ],
82
+ },
83
+ roomField('roomName', 'Name on the sign-in page', 'The name of your app or company, as people know it.'),
84
+ roomField('roomHeadline', 'Headline', 'One short sentence on what the app does for people.'),
85
+ roomField('roomLine', 'Text under the headline', 'A sentence or two that tells people what happens next.', 'longtext'),
86
+ roomField('roomTitle', 'Title above the form', 'For example "Sign in to your account".'),
87
+ roomField('roomDescription', 'Text above the form', 'For example "Enter your email. We send you a code."', 'longtext'),
88
+ {
89
+ key: 'roomSteps',
90
+ label: 'Steps shown on the sign-in page',
91
+ help: `How many steps the page shows, from ${ROOM_STEPS_MIN} to ${ROOM_STEPS_MAX}.`,
92
+ kind: 'number',
93
+ advanced: true,
94
+ max: ROOM_STEPS_MAX,
95
+ showWhen: forCustomers,
96
+ },
97
+ ],
98
+ adds: [
99
+ {
100
+ name: 'A sign-in page',
101
+ hint: 'People sign in and sign up there, then come back to the app.',
102
+ },
103
+ ],
104
+ },
105
+ check(values) {
106
+ const customers = values.users === 'customers';
107
+ if (!customers && values.registration === 'open') {
108
+ return problem('registration', 'Only customers can make their own account. For your team, choose "Only people you invite".');
109
+ }
110
+ if (!customers && values.method === 'email-code') {
111
+ return problem('method', 'A code by email is for customers. Your team signs in with a password.');
112
+ }
113
+ const roomSet = [...Object.keys(ROOM_TEXT_KEYS), 'roomSteps'].some((key) => values[key] !== undefined && values[key] !== '');
114
+ if (!roomSet)
115
+ return null;
116
+ if (!customers) {
117
+ return problem('roomName', 'The words of the sign-in page are only shown to customers.');
118
+ }
119
+ for (const key of ROOM_NEEDED) {
120
+ if (text(values, key) === undefined) {
121
+ const label = signIn.view.fields.find((f) => f.key === key)?.label ?? key;
122
+ return problem(key, `${label} is needed when you change the words of the sign-in page.`);
123
+ }
124
+ }
125
+ for (const key of Object.keys(ROOM_TEXT_KEYS)) {
126
+ const value = text(values, key);
127
+ if (value !== undefined && (value.trim() === '' || ROOM_UNSAFE_TEXT.test(value))) {
128
+ return problem(key, 'Use plain text: no line breaks, "<" or ">".');
129
+ }
130
+ }
131
+ const steps = values.roomSteps;
132
+ if (steps !== undefined &&
133
+ (typeof steps !== 'number' || !Number.isInteger(steps) || steps < ROOM_STEPS_MIN)) {
134
+ return problem('roomSteps', `The steps must be a whole number from ${ROOM_STEPS_MIN} to ${ROOM_STEPS_MAX}.`);
135
+ }
136
+ return null;
137
+ },
138
+ toEntry(entry, values) {
139
+ if (values === null) {
140
+ const next = withUses(entry, omit(entry.uses ?? {}, 'auth'));
141
+ return withWorkerApp(next, (app) => omit(app, 'authEnv'));
142
+ }
143
+ // The parts other modules own (mcp, workspaces) and the operator's replacesBrokerEntry stay.
144
+ const kept = omit(entry.uses?.auth ?? {}, 'signIn', 'room');
145
+ const auth = {
146
+ ...kept,
147
+ users: values.users,
148
+ registration: values.registration,
149
+ };
150
+ if (values.method === 'email-code')
151
+ auth.signIn = 'email-code';
152
+ const room = roomOf(values);
153
+ if (room)
154
+ auth.room = room;
155
+ const next = setUse(entry, 'auth', auth);
156
+ return withWorkerApp(next, (app) => ({ ...app, authEnv: true }));
157
+ },
158
+ fromEntry(entry) {
159
+ const auth = entry.uses?.auth;
160
+ if (!auth)
161
+ return null;
162
+ const values = {
163
+ users: auth.users,
164
+ registration: auth.registration,
165
+ method: auth.signIn ?? 'password',
166
+ };
167
+ const room = auth.room;
168
+ if (room) {
169
+ Object.assign(values, defined({
170
+ roomName: room.name,
171
+ roomHeadline: room.headline,
172
+ roomLine: room.line,
173
+ roomTitle: room.title,
174
+ roomDescription: room.description,
175
+ roomSteps: room.steps,
176
+ }));
177
+ }
178
+ return values;
179
+ },
180
+ };
181
+ function roomOf(values) {
182
+ const name = text(values, 'roomName');
183
+ const headline = text(values, 'roomHeadline');
184
+ const line = text(values, 'roomLine');
185
+ if (name === undefined || headline === undefined || line === undefined)
186
+ return null;
187
+ const room = { name, headline, line };
188
+ const title = text(values, 'roomTitle');
189
+ const description = text(values, 'roomDescription');
190
+ if (title !== undefined)
191
+ room.title = title;
192
+ if (description !== undefined)
193
+ room.description = description;
194
+ if (typeof values.roomSteps === 'number')
195
+ room.steps = values.roomSteps;
196
+ return room;
197
+ }
@@ -0,0 +1,3 @@
1
+ import type { AppModule } from './types.js';
2
+ /** `workerApp.secrets.KEYS_CREDENTIAL = { from: 'keysCustody' }`: the app signs its records with a held key. */
3
+ export declare const signedRecords: AppModule;
@@ -0,0 +1,32 @@
1
+ import { WORKER_APP_KEYS_SECRET } from '../entry-rules.js';
2
+ import { omit, withWorkerApp } from './entry-parts.js';
3
+ /** `workerApp.secrets.KEYS_CREDENTIAL = { from: 'keysCustody' }`: the app signs its records with a held key. */
4
+ export const signedRecords = {
5
+ view: {
6
+ id: 'signed-records',
7
+ name: 'Signed records',
8
+ line: 'The app signs each record it keeps with a key held for your company, so anyone can check later that nothing was changed.',
9
+ category: 'Data',
10
+ icon: 'seal',
11
+ runtimes: ['worker'],
12
+ requires: [],
13
+ availability: 'ready',
14
+ fields: [],
15
+ adds: [
16
+ {
17
+ name: 'A signing key',
18
+ hint: 'Kept in a safe place for this app. The app asks for signatures; it never holds the key.',
19
+ },
20
+ ],
21
+ },
22
+ toEntry(entry, values) {
23
+ return withWorkerApp(entry, (app) => {
24
+ const others = Object.entries(app.secrets ?? {}).filter(([name]) => name !== WORKER_APP_KEYS_SECRET);
25
+ const secrets = Object.fromEntries(values === null ? others : [...others, [WORKER_APP_KEYS_SECRET, { from: 'keysCustody' }]]);
26
+ return Object.keys(secrets).length === 0 ? omit(app, 'secrets') : { ...app, secrets };
27
+ });
28
+ },
29
+ fromEntry(entry) {
30
+ return entry.workerApp?.secrets?.[WORKER_APP_KEYS_SECRET]?.from === 'keysCustody' ? {} : null;
31
+ },
32
+ };
@@ -0,0 +1,194 @@
1
+ import type { HostingEntry } from '../org.js';
2
+ /**
3
+ * The app modules contract (`@wtfalch/foundry/modules`). A module is one part an app can have (sign-in, a
4
+ * database, help for AI assistants). Its view is JSON, so Boule renders any module's form without code of its
5
+ * own; its `toEntry` and `fromEntry` turn the company's answers into a hosting entry's parts and back.
6
+ */
7
+ /** How Boule shows a field. */
8
+ export type FieldKind = 'text' | 'longtext' | 'path' | 'choice' | 'switch' | 'list' | 'number' | 'secret';
9
+ export interface ModuleChoice {
10
+ value: string;
11
+ label: string;
12
+ help?: string;
13
+ /** Shown as "Coming soon" and not selectable. */
14
+ soon?: boolean;
15
+ }
16
+ export interface ModuleField {
17
+ key: string;
18
+ label: string;
19
+ help?: string;
20
+ kind: FieldKind;
21
+ /** For `choice`: the one value picked. For `list`: every item must be one of them. */
22
+ choices?: ModuleChoice[];
23
+ default?: string | number | boolean | string[];
24
+ /** Shown under "Advanced". */
25
+ advanced?: boolean;
26
+ /**
27
+ * The form asks for it. A `secret` field's value is held by the caller, never by the modules or the entry,
28
+ * so `checkModules` does not demand one: the caller checks it is there before it deploys.
29
+ */
30
+ required?: boolean;
31
+ /** A regular expression's source. For a `list` every item must match it. */
32
+ pattern?: string;
33
+ /** The longest text, the most list items, or the largest number. */
34
+ max?: number;
35
+ /** Shown only while another field of the same module has this value. */
36
+ showWhen?: {
37
+ key: string;
38
+ equals: string | boolean;
39
+ };
40
+ /** Shown and accepted only for an app on one of these runtimes. Absent: every runtime the module has. */
41
+ runtimes?: ModuleRuntime[];
42
+ }
43
+ export type ModuleCategory = 'People' | 'Data' | 'Messages' | 'AI' | 'In the background' | 'Business';
44
+ export type ModuleRuntime = 'server' | 'worker';
45
+ /** JSON-safe: Boule renders it. */
46
+ export interface ModuleView {
47
+ id: string;
48
+ name: string;
49
+ /** One plain sentence on what the module gives the app. */
50
+ line: string;
51
+ category: ModuleCategory;
52
+ icon: string;
53
+ /** Where the module works: an app that runs on a server, a Worker app, or both. */
54
+ runtimes: ModuleRuntime[];
55
+ /** Ids of the modules this one needs turned on. */
56
+ requires: string[];
57
+ availability: 'ready' | 'soon';
58
+ fields: ModuleField[];
59
+ /** What turning the module on gives the app, in words. */
60
+ adds: {
61
+ name: string;
62
+ hint: string;
63
+ }[];
64
+ }
65
+ export type ModuleValue = string | number | boolean | string[];
66
+ export type ModuleValues = Record<string, ModuleValue>;
67
+ /** Module id -> its values. A module that is absent is off. */
68
+ export type AppModules = Record<string, ModuleValues>;
69
+ export interface ModuleContext {
70
+ runtime: ModuleRuntime;
71
+ }
72
+ /** Why a module or one of its fields was refused, in plain words. */
73
+ export interface ModuleProblem {
74
+ field: string | null;
75
+ message: string;
76
+ }
77
+ export interface ModuleRefusal extends ModuleProblem {
78
+ module: string;
79
+ }
80
+ /** One module: one file `modules/<id>.ts`, listed in `modules/index.ts`. */
81
+ export interface AppModule {
82
+ view: ModuleView;
83
+ /**
84
+ * The entry with this module's parts set from `values` (checked, defaults filled), or with every part the
85
+ * module owns removed when `values` is null. Never changes `entry` itself.
86
+ */
87
+ toEntry(entry: HostingEntry, values: ModuleValues | null, ctx: ModuleContext): HostingEntry | ModuleProblem;
88
+ /** The module's values as the entry holds them, or null when the entry does not have the module. */
89
+ fromEntry(entry: HostingEntry): ModuleValues | null;
90
+ /** Rules between fields, or with other modules, beyond what the fields themselves say. */
91
+ check?(values: ModuleValues, modules: AppModules, ctx: ModuleContext): ModuleProblem | null;
92
+ }
93
+ /** How a manifest secret gets its value. */
94
+ export type ManifestSecretKind = 'generated' | 'held';
95
+ /** A setting the company may change, delivered to the app as a plain value. */
96
+ export interface ManifestSetting {
97
+ label: string;
98
+ help?: string;
99
+ default?: string;
100
+ }
101
+ /** `company-app.json` v1: what the app's code asks for, written once by its developer. */
102
+ export interface AppManifestV1 {
103
+ v: 1;
104
+ runtime: ModuleRuntime;
105
+ modules: AppModules;
106
+ secrets: Record<string, ManifestSecretKind>;
107
+ settings: Record<string, ManifestSetting>;
108
+ }
109
+ /** How a generated value is made. All are random, made once at install and kept across updates. */
110
+ export type ManifestGenerateKind = 'token' | 'hex' | 'uuid';
111
+ /**
112
+ * The words Boule shows for a value. `label` and `help` are plain English and always present; `labelKey` and
113
+ * `helpKey` are optional copy keys (`@wtfalch/i18n`) that Boule prefers when it has a catalogue for them.
114
+ */
115
+ export interface ManifestCopy {
116
+ label: string;
117
+ help?: string;
118
+ labelKey?: string;
119
+ helpKey?: string;
120
+ }
121
+ export interface ManifestOption {
122
+ value: string;
123
+ label: string;
124
+ help?: string;
125
+ labelKey?: string;
126
+ }
127
+ /** Made by the platform at install. Always a secret; never shown. */
128
+ export interface ManifestGenerate {
129
+ source: 'generate';
130
+ kind: ManifestGenerateKind;
131
+ }
132
+ /** Taken from what another part of the app produces, such as the database's address. Never asked. */
133
+ export interface ManifestReference {
134
+ source: 'reference';
135
+ /** `<module id>.<output>`, such as `database.address`, or `app.<output>` / `company.<output>`. */
136
+ to: string;
137
+ secret?: boolean;
138
+ }
139
+ /** A value the app ships with and the company may change later. Never a secret. */
140
+ export interface ManifestDefault {
141
+ source: 'default';
142
+ value: string;
143
+ /** Plain words on what the value does. */
144
+ description: string;
145
+ label?: string;
146
+ labelKey?: string;
147
+ }
148
+ /**
149
+ * Asked of the company at install, in plain words. `required` blocks the install until it is answered.
150
+ * With `into` the answer goes to a field of a module (`{ module: 'sign-in', field: 'registration' }`) and the
151
+ * key is a free lowercase id; without it the key is the environment name the app reads.
152
+ */
153
+ export interface ManifestAsk extends ManifestCopy {
154
+ source: 'ask';
155
+ required?: boolean;
156
+ /** The company types a secret: it is never in this file, so no default. */
157
+ secret?: boolean;
158
+ default?: string | string[];
159
+ /** The answer must be one of these (a list setting when `multiple`). */
160
+ choices?: ManifestOption[];
161
+ multiple?: boolean;
162
+ pattern?: string;
163
+ max?: number;
164
+ into?: {
165
+ module: string;
166
+ field: string;
167
+ };
168
+ }
169
+ export type ManifestValue = ManifestGenerate | ManifestReference | ManifestDefault | ManifestAsk;
170
+ /** A place the app says it reaches. Advice shown to the company; never enforced and never needs approval. */
171
+ export interface ManifestHost {
172
+ /** A host name such as `api.example.com`, or `*.example.com` for every subdomain. */
173
+ host: string;
174
+ /** Plain words on why. */
175
+ why: string;
176
+ }
177
+ /**
178
+ * Advisory only. The company runs its own app on its own network and manages its own access, so the app may connect
179
+ * anywhere; `hosts` is a recommendation shown to the company, not a limit.
180
+ */
181
+ export interface ManifestAccess {
182
+ hosts: ManifestHost[];
183
+ }
184
+ /** `company-app.json` v2, and what every manifest (v1 too) is read as. */
185
+ export interface AppManifest {
186
+ v: 2;
187
+ runtime: ModuleRuntime;
188
+ modules: AppModules;
189
+ values: Record<string, ManifestValue>;
190
+ /** Optional. Absent means the app may connect anywhere. */
191
+ access?: ManifestAccess;
192
+ }
193
+ /** An answer to an `ask`: text, or a list of texts for `multiple`. */
194
+ export type ManifestAnswer = string | string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { AppModule } from './types.js';
2
+ /** `uses.auth.workspaces`: the app's customers make shared workspaces and invite people into them. */
3
+ export declare const workspaces: AppModule;
@@ -0,0 +1,75 @@
1
+ import { WORKSPACE_ROLE, WORKSPACE_ROLES_MAX, isPagePath } from '../entry-rules.js';
2
+ import { list, problem, setAuthPart } from './entry-parts.js';
3
+ /** `uses.auth.workspaces`: the app's customers make shared workspaces and invite people into them. */
4
+ export const workspaces = {
5
+ view: {
6
+ id: 'workspaces',
7
+ name: 'Workspaces',
8
+ line: 'Your customers make a shared space, invite the people they work with and give each one a role.',
9
+ category: 'People',
10
+ icon: 'users',
11
+ runtimes: ['server'],
12
+ requires: ['sign-in'],
13
+ availability: 'ready',
14
+ fields: [
15
+ {
16
+ key: 'roles',
17
+ label: 'Roles in a workspace',
18
+ help: 'The person who makes a workspace gets the first role. Lowercase letters, digits, "_" and "-".',
19
+ kind: 'list',
20
+ required: true,
21
+ default: ['owner', 'member'],
22
+ pattern: WORKSPACE_ROLE.source,
23
+ max: WORKSPACE_ROLES_MAX,
24
+ },
25
+ {
26
+ key: 'invitePath',
27
+ label: 'Page that opens an invitation',
28
+ help: 'The page on your app the invitation email links to, such as /join.',
29
+ kind: 'path',
30
+ advanced: true,
31
+ required: true,
32
+ default: '/join',
33
+ },
34
+ ],
35
+ adds: [
36
+ { name: 'Invitations by email', hint: 'Sent from your company, with a link to the app.' },
37
+ ],
38
+ },
39
+ check(values, modules) {
40
+ if (modules['sign-in']?.users !== 'customers') {
41
+ return problem(null, 'Workspaces are for your customers. In Sign-in, choose "Your customers" first.');
42
+ }
43
+ const roles = list(values, 'roles') ?? [];
44
+ const seen = new Set();
45
+ for (const [index, role] of roles.entries()) {
46
+ if (role.startsWith('c_')) {
47
+ return problem('roles', `The role "${role}" may not start with "c_".`);
48
+ }
49
+ if (seen.has(role))
50
+ return problem('roles', `The role "${role}" is named twice.`);
51
+ if (role === 'self_service' || (role === 'owner' && index > 0)) {
52
+ return problem('roles', `The role "${role}" is kept for the system: only the first role may be called "owner", and "self_service" is never allowed.`);
53
+ }
54
+ seen.add(role);
55
+ }
56
+ if (!isPagePath(values.invitePath)) {
57
+ return problem('invitePath', 'The page must start with "/", such as /join, with no spaces, "?", "#" or "..".');
58
+ }
59
+ return null;
60
+ },
61
+ toEntry(entry, values) {
62
+ if (values === null)
63
+ return setAuthPart(entry, 'workspaces', undefined);
64
+ if (!entry.uses?.auth)
65
+ return problem(null, 'Workspaces need Sign-in. Turn on Sign-in first.');
66
+ return setAuthPart(entry, 'workspaces', {
67
+ roles: list(values, 'roles') ?? [],
68
+ invitePath: values.invitePath,
69
+ });
70
+ },
71
+ fromEntry(entry) {
72
+ const used = entry.uses?.auth?.workspaces;
73
+ return used ? { roles: [...used.roles], invitePath: used.invitePath } : null;
74
+ },
75
+ };