@wtfalch/foundry 0.76.1 → 0.77.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/app-canary-worker.d.ts +35 -0
- package/dist/app-canary-worker.js +56 -0
- package/dist/audit/connector.d.ts +6 -2
- package/dist/audit/connector.js +3 -2
- package/dist/audit/evidence.d.ts +4 -0
- package/dist/audit/evidence.js +14 -2
- package/dist/audit/host-store.d.ts +58 -0
- package/dist/audit/host-store.js +147 -0
- package/dist/audit/host.d.ts +53 -0
- package/dist/audit/host.js +294 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.js +2 -0
- package/dist/audit/registry.d.ts +14 -0
- package/dist/audit/sealer-role.d.ts +12 -0
- package/dist/audit/sealer-role.js +26 -2
- package/dist/audit/tick.d.ts +10 -2
- package/dist/audit/tick.js +15 -2
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/entry-rules.d.ts +35 -0
- package/dist/entry-rules.js +35 -0
- package/dist/hosted-label.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/migrations/0072_audit_host.sql +46 -0
- package/dist/migrations/0074_platform_app_canary.sql +11 -0
- package/dist/modules/ai.js +15 -1
- package/dist/modules/assistants.js +3 -0
- package/dist/modules/email.d.ts +6 -1
- package/dist/modules/email.js +88 -5
- package/dist/modules/files.js +79 -2
- package/dist/modules/index.js +7 -0
- package/dist/modules/jobs.d.ts +5 -1
- package/dist/modules/jobs.js +74 -4
- package/dist/modules/reporting.d.ts +9 -0
- package/dist/modules/reporting.js +29 -0
- package/dist/modules/sign-in.js +20 -2
- package/dist/modules/types.d.ts +2 -0
- package/dist/modules/workspaces.js +3 -0
- package/dist/org.d.ts +114 -13
- package/dist/org.js +233 -20
- package/dist/plan/ai-boule-key-apply.d.ts +65 -0
- package/dist/plan/ai-boule-key-apply.js +169 -0
- package/dist/plan/ai-boule-key.d.ts +89 -0
- package/dist/plan/ai-boule-key.js +195 -0
- package/dist/plan/analytics.d.ts +8 -1
- package/dist/plan/analytics.js +8 -1
- package/dist/plan/app-canary-apply.d.ts +49 -0
- package/dist/plan/app-canary-apply.js +232 -0
- package/dist/plan/app-canary.d.ts +44 -0
- package/dist/plan/app-canary.js +110 -0
- package/dist/plan/blue-green-apply.d.ts +6 -2
- package/dist/plan/blue-green-apply.js +14 -3
- package/dist/plan/broker-entries.js +20 -5
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +22 -0
- package/dist/plan/diff.js +231 -18
- package/dist/plan/entry-files-key.d.ts +39 -3
- package/dist/plan/entry-files-key.js +103 -6
- package/dist/plan/entry-jobs-boule-key-apply.d.ts +65 -0
- package/dist/plan/entry-jobs-boule-key-apply.js +192 -0
- package/dist/plan/entry-jobs-boule-key.d.ts +59 -0
- package/dist/plan/entry-jobs-boule-key.js +141 -0
- package/dist/plan/entry-jobs.d.ts +2 -0
- package/dist/plan/entry-reporting-key.d.ts +36 -0
- package/dist/plan/entry-reporting-key.js +136 -0
- package/dist/plan/executor.d.ts +108 -1
- package/dist/plan/executor.js +669 -23
- package/dist/plan/files-boule-key-apply.d.ts +73 -0
- package/dist/plan/files-boule-key-apply.js +223 -0
- package/dist/plan/files-boule-key.d.ts +92 -0
- package/dist/plan/files-boule-key.js +175 -0
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/jobs-scheduler-http.d.ts +13 -0
- package/dist/plan/jobs-scheduler-http.js +39 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +4 -1
- package/dist/plan/login-host.js +2 -2
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-diff.js +3 -2
- package/dist/plan/own-jobs-route.d.ts +5 -2
- package/dist/plan/own-jobs-route.js +6 -2
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +24 -0
- package/dist/plan/plan-apply.js +39 -4
- package/dist/plan/platform-diff.d.ts +2 -0
- package/dist/plan/platform-diff.js +15 -1
- package/dist/plan/platform-providers.d.ts +9 -0
- package/dist/plan/platform-providers.js +21 -0
- package/dist/plan/providers-readers.js +25 -1
- package/dist/plan/registered-app-env.d.ts +31 -0
- package/dist/plan/registered-app-env.js +63 -0
- package/dist/plan/registered-app.d.ts +121 -0
- package/dist/plan/registered-app.js +262 -0
- package/dist/plan/release-host.js +2 -1
- package/dist/plan/reporting-boule.d.ts +18 -0
- package/dist/plan/reporting-boule.js +44 -0
- package/dist/plan/reporting.d.ts +90 -0
- package/dist/plan/reporting.js +135 -0
- package/dist/plan/stage-env.js +1 -0
- package/dist/plan/stage-isolation.d.ts +3 -1
- package/dist/plan/stage-isolation.js +20 -2
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +8 -1
- package/dist/plan/stage-sink.d.ts +2 -2
- package/dist/plan/stage-sink.js +2 -2
- package/dist/plan/workspace-client.d.ts +1 -1
- package/dist/plan/workspace-client.js +2 -1
- package/dist/plan/workspace-sign-in.d.ts +1 -1
- package/dist/plan/workspace-sign-in.js +39 -7
- package/dist/platform.d.ts +25 -0
- package/dist/platform.js +20 -0
- package/dist/service-labels.d.ts +8 -0
- package/dist/service-labels.js +8 -0
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/store/postgres.js +13 -0
- package/dist/store/schema.d.ts +485 -2
- package/dist/store/schema.js +38 -1
- package/dist/worker-app-config.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the audit host (foundry#1110). Three new tables, nothing existing touched, so old and new
|
|
2
|
+
-- releases run side by side. Mirrors src/store/schema.ts's `foundryAuditHostKeys`,
|
|
3
|
+
-- `foundryAuditLedgerRegistrations` and `foundryAuditSealResults`.
|
|
4
|
+
--
|
|
5
|
+
-- A company's audit host key: only its sha256 is kept; the key itself goes to the company's Boule once.
|
|
6
|
+
create table if not exists foundry_audit_host_keys (
|
|
7
|
+
org_id text primary key,
|
|
8
|
+
key_sha256 text not null unique,
|
|
9
|
+
created_at timestamptz not null default now()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
-- A ledger a company's Boule registered (`PUT /v1/audit/ledgers/:app`): where its live database is and the
|
|
13
|
+
-- sealing role it is opened as. The password is not here: it is in the host's held keys at `password_ref`.
|
|
14
|
+
create table if not exists foundry_audit_ledger_registrations (
|
|
15
|
+
org_id text not null,
|
|
16
|
+
entry_key text not null,
|
|
17
|
+
copy text not null,
|
|
18
|
+
db_host text not null,
|
|
19
|
+
db_port integer not null,
|
|
20
|
+
db_name text not null,
|
|
21
|
+
db_user text not null,
|
|
22
|
+
sslmode text,
|
|
23
|
+
password_ref text not null,
|
|
24
|
+
created_at timestamptz not null default now(),
|
|
25
|
+
updated_at timestamptz not null default now(),
|
|
26
|
+
primary key (org_id, entry_key)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- Each seal tick's result per ledger, kept for 12 hours (`SEAL_RESULTS_KEPT_HOURS`): what `status` shows.
|
|
30
|
+
create table if not exists foundry_audit_seal_results (
|
|
31
|
+
id bigserial primary key,
|
|
32
|
+
org_id text not null,
|
|
33
|
+
entry_key text not null,
|
|
34
|
+
ledger text not null,
|
|
35
|
+
outcome text not null,
|
|
36
|
+
tree_size integer,
|
|
37
|
+
anchored integer not null default 0,
|
|
38
|
+
erasures integer not null default 0,
|
|
39
|
+
reason text,
|
|
40
|
+
at timestamptz not null
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
create index if not exists foundry_audit_seal_results_ledger_idx
|
|
44
|
+
on foundry_audit_seal_results (org_id, entry_key, at desc, id desc);
|
|
45
|
+
create index if not exists foundry_audit_seal_results_at_idx
|
|
46
|
+
on foundry_audit_seal_results (at);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the app-layer canary on the declared platform (foundry#1133).
|
|
2
|
+
-- Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` additions.
|
|
3
|
+
-- `app_canary_tag` and `app_canary_sha256` are the pin of the canary Worker
|
|
4
|
+
-- foundry ships (`appCanaryShippedPin()`); both empty means nothing is pinned
|
|
5
|
+
-- yet, and the planner reports that instead of planning the step. Off by
|
|
6
|
+
-- default, like the usage tail Worker.
|
|
7
|
+
|
|
8
|
+
alter table foundry_declared_platform
|
|
9
|
+
add column if not exists app_canary_enabled boolean not null default false,
|
|
10
|
+
add column if not exists app_canary_tag text not null default '',
|
|
11
|
+
add column if not exists app_canary_sha256 text not null default '';
|
package/dist/modules/ai.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { AI_ENTRY_PERMISSIONS, AI_WEBHOOK_PATH } from '../entry-rules.js';
|
|
2
2
|
import { list, setUse, text } from './entry-parts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Plain words for every permission an app's AI key may hold. A `Record` over the permission list, so a
|
|
5
|
+
* permission added to {@link AI_ENTRY_PERMISSIONS} fails the typecheck until it has words here.
|
|
6
|
+
*/
|
|
7
|
+
const PERMISSION_WORDS = {
|
|
8
|
+
'ai.offerings:invoke': {
|
|
9
|
+
label: 'Use the AI tasks and wait for the answer',
|
|
10
|
+
help: 'The app asks an AI task and gets the answer back at once.',
|
|
11
|
+
},
|
|
12
|
+
'ai.runs:create': {
|
|
13
|
+
label: 'Start AI work that finishes later',
|
|
14
|
+
help: 'The app starts longer AI work and gets the result when it is done.',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
3
17
|
/** `uses.ai`: the app calls the AI tasks set up for the company. */
|
|
4
18
|
export const ai = {
|
|
5
19
|
view: {
|
|
@@ -33,7 +47,7 @@ export const ai = {
|
|
|
33
47
|
help: 'Leave it empty to allow all of them.',
|
|
34
48
|
kind: 'list',
|
|
35
49
|
advanced: true,
|
|
36
|
-
choices: AI_ENTRY_PERMISSIONS.map((value) => ({ value,
|
|
50
|
+
choices: AI_ENTRY_PERMISSIONS.map((value) => ({ value, ...PERMISSION_WORDS[value] })),
|
|
37
51
|
},
|
|
38
52
|
],
|
|
39
53
|
adds: [{ name: 'AI tasks', hint: "Run on your company's own AI account." }],
|
|
@@ -57,6 +57,9 @@ export const assistants = {
|
|
|
57
57
|
if (signIn?.users !== 'customers') {
|
|
58
58
|
return problem(null, 'Help for AI assistants needs Sign-in for your customers: each customer connects their own assistant. In Sign-in, choose "Your customers". They can sign up themselves or be invited.');
|
|
59
59
|
}
|
|
60
|
+
if (signIn.team === true) {
|
|
61
|
+
return problem(null, 'Help for AI assistants is not ready yet for an app your team signs in to as well. In Sign-in, turn off "Your team signs in too".');
|
|
62
|
+
}
|
|
60
63
|
const onboarding = text(values, 'onboardingPath');
|
|
61
64
|
if (onboarding !== undefined && !isPagePath(onboarding)) {
|
|
62
65
|
return problem('onboardingPath', 'The page must start with "/", such as /onboarding, with no spaces, "?", "#" or "..".');
|
package/dist/modules/email.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { AppModule } from './types.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* `uses.mail`: the app sends mail from the company's mail domain. With every setting at its default the entry
|
|
4
|
+
* holds `uses.mail: true`, as before; otherwise an {@link EntryMail} holding only the settings that differ.
|
|
5
|
+
* The sender name and reply-to go to the app's env (`entrySettingsEnv`); the bounce settings are for the
|
|
6
|
+
* company's mail service.
|
|
7
|
+
*/
|
|
3
8
|
export declare const email: AppModule;
|
package/dist/modules/email.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { MAIL_BOUNCES_DEFAULT, MAIL_BOUNCES_MAX, MAIL_BOUNCE_ALERT_DEFAULT, MAIL_FROM_NAME, MAIL_FROM_NAME_MAX, MAIL_REPLY_TO, MAIL_REPLY_TO_MAX, PERCENT_MAX, PERCENT_MIN, } from '../entry-rules.js';
|
|
2
|
+
import { problem, setUse, text } from './entry-parts.js';
|
|
3
|
+
/**
|
|
4
|
+
* `uses.mail`: the app sends mail from the company's mail domain. With every setting at its default the entry
|
|
5
|
+
* holds `uses.mail: true`, as before; otherwise an {@link EntryMail} holding only the settings that differ.
|
|
6
|
+
* The sender name and reply-to go to the app's env (`entrySettingsEnv`); the bounce settings are for the
|
|
7
|
+
* company's mail service.
|
|
8
|
+
*/
|
|
3
9
|
export const email = {
|
|
4
10
|
view: {
|
|
5
11
|
id: 'email',
|
|
@@ -10,9 +16,86 @@ export const email = {
|
|
|
10
16
|
runtimes: ['server'],
|
|
11
17
|
requires: [],
|
|
12
18
|
availability: 'ready',
|
|
13
|
-
fields: [
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
key: 'fromName',
|
|
22
|
+
label: 'Sender name',
|
|
23
|
+
help: "The name people see the email come from. Leave it empty to use your company's name.",
|
|
24
|
+
kind: 'text',
|
|
25
|
+
max: MAIL_FROM_NAME_MAX,
|
|
26
|
+
pattern: MAIL_FROM_NAME.source,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'replyTo',
|
|
30
|
+
label: 'Replies go to',
|
|
31
|
+
help: 'An email address, such as help@example.com. Leave it empty to send replies to the sending address.',
|
|
32
|
+
kind: 'text',
|
|
33
|
+
max: MAIL_REPLY_TO_MAX,
|
|
34
|
+
pattern: MAIL_REPLY_TO.source,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'stopAfterBounces',
|
|
38
|
+
label: 'Stop sending to an address after this many bounces',
|
|
39
|
+
help: `An email bounces when it cannot be delivered. From 1 to ${MAIL_BOUNCES_MAX}.`,
|
|
40
|
+
kind: 'number',
|
|
41
|
+
default: MAIL_BOUNCES_DEFAULT,
|
|
42
|
+
min: 1,
|
|
43
|
+
max: MAIL_BOUNCES_MAX,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'bounceAlertPercent',
|
|
47
|
+
label: 'Email the owners when more than this share of emails bounce',
|
|
48
|
+
help: `In percent of a week's email, from ${PERCENT_MIN} to ${PERCENT_MAX}.`,
|
|
49
|
+
kind: 'number',
|
|
50
|
+
default: MAIL_BOUNCE_ALERT_DEFAULT,
|
|
51
|
+
min: PERCENT_MIN,
|
|
52
|
+
max: PERCENT_MAX,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
14
55
|
adds: [{ name: 'Sending email', hint: "From your company's own mail address." }],
|
|
15
56
|
},
|
|
16
|
-
|
|
17
|
-
|
|
57
|
+
check(values) {
|
|
58
|
+
const name = text(values, 'fromName');
|
|
59
|
+
if (name !== undefined && name.trim() !== name) {
|
|
60
|
+
return problem('fromName', '"Sender name" cannot start or end with a space.');
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
},
|
|
64
|
+
toEntry(entry, values) {
|
|
65
|
+
if (values === null)
|
|
66
|
+
return setUse(entry, 'mail', undefined);
|
|
67
|
+
const next = {};
|
|
68
|
+
const fromName = text(values, 'fromName');
|
|
69
|
+
const replyTo = text(values, 'replyTo');
|
|
70
|
+
if (fromName !== undefined)
|
|
71
|
+
next.fromName = fromName;
|
|
72
|
+
if (replyTo !== undefined)
|
|
73
|
+
next.replyTo = replyTo;
|
|
74
|
+
const bounces = values.stopAfterBounces;
|
|
75
|
+
if (typeof bounces === 'number' && bounces !== MAIL_BOUNCES_DEFAULT) {
|
|
76
|
+
next.stopAfterBounces = bounces;
|
|
77
|
+
}
|
|
78
|
+
const alert = values.bounceAlertPercent;
|
|
79
|
+
if (typeof alert === 'number' && alert !== MAIL_BOUNCE_ALERT_DEFAULT) {
|
|
80
|
+
next.bounceAlertPercent = alert;
|
|
81
|
+
}
|
|
82
|
+
return setUse(entry, 'mail', Object.keys(next).length === 0 ? true : next);
|
|
83
|
+
},
|
|
84
|
+
fromEntry(entry) {
|
|
85
|
+
const used = entry.uses?.mail;
|
|
86
|
+
if (!used)
|
|
87
|
+
return null;
|
|
88
|
+
if (used === true)
|
|
89
|
+
return {};
|
|
90
|
+
const values = {};
|
|
91
|
+
if (used.fromName !== undefined)
|
|
92
|
+
values.fromName = used.fromName;
|
|
93
|
+
if (used.replyTo !== undefined)
|
|
94
|
+
values.replyTo = used.replyTo;
|
|
95
|
+
if (used.stopAfterBounces !== undefined)
|
|
96
|
+
values.stopAfterBounces = used.stopAfterBounces;
|
|
97
|
+
if (used.bounceAlertPercent !== undefined)
|
|
98
|
+
values.bounceAlertPercent = used.bounceAlertPercent;
|
|
99
|
+
return values;
|
|
100
|
+
},
|
|
18
101
|
};
|
package/dist/modules/files.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
|
-
import { FILES_COLLECTION_NAME, FILES_ENTRY_PERMISSIONS } from '../entry-rules.js';
|
|
1
|
+
import { FILES_COLLECTION_NAME, FILES_ENTRY_PERMISSIONS, FILES_SHARE_LINK_DAYS, FILES_SHARE_LINK_DAYS_DEFAULT, FILES_SPACE_ALERT_DEFAULT, PERCENT_MAX, PERCENT_MIN, } from '../entry-rules.js';
|
|
2
2
|
import { list, setUse } from './entry-parts.js';
|
|
3
|
+
/**
|
|
4
|
+
* Plain words for every permission an app's files key may hold. A `Record` over the permission list, so a
|
|
5
|
+
* permission added to {@link FILES_ENTRY_PERMISSIONS} fails the typecheck until it has words here.
|
|
6
|
+
*/
|
|
7
|
+
const PERMISSION_WORDS = {
|
|
8
|
+
'files.collections:read': {
|
|
9
|
+
label: 'See the folders',
|
|
10
|
+
help: 'The app lists its folders and what each one holds.',
|
|
11
|
+
},
|
|
12
|
+
'files.files:read': {
|
|
13
|
+
label: 'See the files',
|
|
14
|
+
help: 'The app reads the names and details of the files.',
|
|
15
|
+
},
|
|
16
|
+
'files.files:download': {
|
|
17
|
+
label: 'Open files',
|
|
18
|
+
help: 'The app gets the content of a file to show or send it.',
|
|
19
|
+
},
|
|
20
|
+
'files.files:upload': { label: 'Add files', help: 'The app saves new files.' },
|
|
21
|
+
'files.files:replace': {
|
|
22
|
+
label: 'Replace files',
|
|
23
|
+
help: 'The app saves a new version over a file it already has.',
|
|
24
|
+
},
|
|
25
|
+
'files.files:delete': { label: 'Delete files', help: 'The app removes files.' },
|
|
26
|
+
};
|
|
27
|
+
const NEVER = 'never';
|
|
28
|
+
const dayWords = (days) => (days === 1 ? '1 day' : `${days} days`);
|
|
3
29
|
/** `uses.files`: the app keeps files (uploads, documents) in folders of its own. */
|
|
4
30
|
export const files = {
|
|
5
31
|
view: {
|
|
@@ -20,6 +46,37 @@ export const files = {
|
|
|
20
46
|
required: true,
|
|
21
47
|
pattern: FILES_COLLECTION_NAME.source,
|
|
22
48
|
},
|
|
49
|
+
{
|
|
50
|
+
key: 'shareLinkDays',
|
|
51
|
+
label: 'Share links stop working after',
|
|
52
|
+
help: 'How long a link to a shared file works, unless the person who shares it picks another time.',
|
|
53
|
+
kind: 'choice',
|
|
54
|
+
default: String(FILES_SHARE_LINK_DAYS_DEFAULT),
|
|
55
|
+
choices: [
|
|
56
|
+
...FILES_SHARE_LINK_DAYS.map((days) => ({ value: String(days), label: dayWords(days) })),
|
|
57
|
+
{
|
|
58
|
+
value: NEVER,
|
|
59
|
+
label: 'Never',
|
|
60
|
+
help: 'A link works until someone stops sharing the file.',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'shareLinkPick',
|
|
66
|
+
label: 'People may pick another time when they share',
|
|
67
|
+
help: 'Off: every link stops working after the time above.',
|
|
68
|
+
kind: 'switch',
|
|
69
|
+
default: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'spaceAlertPercent',
|
|
73
|
+
label: 'Email the owners when this much of the space is used',
|
|
74
|
+
help: `In percent, from ${PERCENT_MIN} to ${PERCENT_MAX}.`,
|
|
75
|
+
kind: 'number',
|
|
76
|
+
default: FILES_SPACE_ALERT_DEFAULT,
|
|
77
|
+
min: PERCENT_MIN,
|
|
78
|
+
max: PERCENT_MAX,
|
|
79
|
+
},
|
|
23
80
|
{
|
|
24
81
|
key: 'shareHost',
|
|
25
82
|
label: 'Share links on your own address',
|
|
@@ -34,7 +91,7 @@ export const files = {
|
|
|
34
91
|
help: 'Leave it empty to allow all of them.',
|
|
35
92
|
kind: 'list',
|
|
36
93
|
advanced: true,
|
|
37
|
-
choices: FILES_ENTRY_PERMISSIONS.map((value) => ({ value,
|
|
94
|
+
choices: FILES_ENTRY_PERMISSIONS.map((value) => ({ value, ...PERMISSION_WORDS[value] })),
|
|
38
95
|
},
|
|
39
96
|
],
|
|
40
97
|
adds: [{ name: 'File storage', hint: 'Only this app reaches its folders.' }],
|
|
@@ -48,6 +105,19 @@ export const files = {
|
|
|
48
105
|
next.permissions = permissions;
|
|
49
106
|
if (values.shareHost === true)
|
|
50
107
|
next.shareHost = true;
|
|
108
|
+
// A setting at its default is left out, so an entry saved before these settings plans as it did.
|
|
109
|
+
const days = values.shareLinkDays;
|
|
110
|
+
if (days === NEVER)
|
|
111
|
+
next.shareLinkDays = null;
|
|
112
|
+
else if (typeof days === 'string' && days !== String(FILES_SHARE_LINK_DAYS_DEFAULT)) {
|
|
113
|
+
next.shareLinkDays = Number(days);
|
|
114
|
+
}
|
|
115
|
+
if (values.shareLinkPick === false)
|
|
116
|
+
next.shareLinkPick = false;
|
|
117
|
+
const alert = values.spaceAlertPercent;
|
|
118
|
+
if (typeof alert === 'number' && alert !== FILES_SPACE_ALERT_DEFAULT) {
|
|
119
|
+
next.spaceAlertPercent = alert;
|
|
120
|
+
}
|
|
51
121
|
return setUse(entry, 'files', next);
|
|
52
122
|
},
|
|
53
123
|
fromEntry(entry) {
|
|
@@ -59,6 +129,13 @@ export const files = {
|
|
|
59
129
|
values.permissions = [...used.permissions];
|
|
60
130
|
if (used.shareHost !== undefined)
|
|
61
131
|
values.shareHost = used.shareHost;
|
|
132
|
+
if (used.shareLinkDays !== undefined) {
|
|
133
|
+
values.shareLinkDays = used.shareLinkDays === null ? NEVER : String(used.shareLinkDays);
|
|
134
|
+
}
|
|
135
|
+
if (used.shareLinkPick !== undefined)
|
|
136
|
+
values.shareLinkPick = used.shareLinkPick;
|
|
137
|
+
if (used.spaceAlertPercent !== undefined)
|
|
138
|
+
values.spaceAlertPercent = used.spaceAlertPercent;
|
|
62
139
|
return values;
|
|
63
140
|
},
|
|
64
141
|
};
|
package/dist/modules/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { files } from './files.js';
|
|
|
11
11
|
import { jobs } from './jobs.js';
|
|
12
12
|
import { organisations } from './organisations.js';
|
|
13
13
|
import { SECRET_OUTPUTS, referenceProblem } from './outputs.js';
|
|
14
|
+
import { reporting } from './reporting.js';
|
|
14
15
|
import { repository } from './repository.js';
|
|
15
16
|
import { signIn } from './sign-in.js';
|
|
16
17
|
import { signedRecords } from './signed-records.js';
|
|
@@ -30,6 +31,7 @@ export const MODULES = [
|
|
|
30
31
|
email,
|
|
31
32
|
jobs,
|
|
32
33
|
analytics,
|
|
34
|
+
reporting,
|
|
33
35
|
ai,
|
|
34
36
|
organisations,
|
|
35
37
|
audit,
|
|
@@ -65,6 +67,11 @@ function fieldProblem(field, value) {
|
|
|
65
67
|
case 'number':
|
|
66
68
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
67
69
|
return `${name} must be a number.`;
|
|
70
|
+
if (!Number.isInteger(value))
|
|
71
|
+
return `${name} must be a whole number.`;
|
|
72
|
+
if (field.min !== undefined && value < field.min) {
|
|
73
|
+
return `${name} must be at least ${field.min}.`;
|
|
74
|
+
}
|
|
68
75
|
return field.max !== undefined && value > field.max
|
|
69
76
|
? `${name} may be at most ${field.max}.`
|
|
70
77
|
: null;
|
package/dist/modules/jobs.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { AppModule } from './types.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* `uses.jobs`: the app is a project of its own in the jobs scheduler. Its settings are for that project: with
|
|
4
|
+
* every one at its default the entry holds `uses.jobs: true`, as before; otherwise an {@link EntryJobs}
|
|
5
|
+
* holding only the settings that differ.
|
|
6
|
+
*/
|
|
3
7
|
export declare const jobs: AppModule;
|
package/dist/modules/jobs.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { JOBS_PAUSE_AFTER_DEFAULT, JOBS_PAUSE_AFTER_MAX, JOBS_RETRIES_DEFAULT, JOBS_RETRIES_MAX, } from '../entry-rules.js';
|
|
1
2
|
import { setUse } from './entry-parts.js';
|
|
2
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* `uses.jobs`: the app is a project of its own in the jobs scheduler. Its settings are for that project: with
|
|
5
|
+
* every one at its default the entry holds `uses.jobs: true`, as before; otherwise an {@link EntryJobs}
|
|
6
|
+
* holding only the settings that differ.
|
|
7
|
+
*/
|
|
3
8
|
export const jobs = {
|
|
4
9
|
view: {
|
|
5
10
|
id: 'jobs',
|
|
@@ -10,7 +15,40 @@ export const jobs = {
|
|
|
10
15
|
runtimes: ['server'],
|
|
11
16
|
requires: [],
|
|
12
17
|
availability: 'ready',
|
|
13
|
-
fields: [
|
|
18
|
+
fields: [
|
|
19
|
+
{
|
|
20
|
+
key: 'retries',
|
|
21
|
+
label: 'Tries again when a job fails',
|
|
22
|
+
help: `How many more times we try a job that failed, from 0 to ${JOBS_RETRIES_MAX}.`,
|
|
23
|
+
kind: 'number',
|
|
24
|
+
default: JOBS_RETRIES_DEFAULT,
|
|
25
|
+
min: 0,
|
|
26
|
+
max: JOBS_RETRIES_MAX,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'alertOwners',
|
|
30
|
+
label: 'Email the owners when a job failed every try',
|
|
31
|
+
kind: 'switch',
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'pauseOnFailures',
|
|
36
|
+
label: 'Pause a job that keeps failing',
|
|
37
|
+
help: 'A paused job waits until someone resumes it on the Jobs page.',
|
|
38
|
+
kind: 'switch',
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'pauseAfterFailures',
|
|
43
|
+
label: 'Pause it after this many failed runs in a row',
|
|
44
|
+
help: `From 1 to ${JOBS_PAUSE_AFTER_MAX}.`,
|
|
45
|
+
kind: 'number',
|
|
46
|
+
default: JOBS_PAUSE_AFTER_DEFAULT,
|
|
47
|
+
min: 1,
|
|
48
|
+
max: JOBS_PAUSE_AFTER_MAX,
|
|
49
|
+
showWhen: { key: 'pauseOnFailures', equals: true },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
14
52
|
adds: [
|
|
15
53
|
{
|
|
16
54
|
name: 'A scheduler',
|
|
@@ -18,6 +56,38 @@ export const jobs = {
|
|
|
18
56
|
},
|
|
19
57
|
],
|
|
20
58
|
},
|
|
21
|
-
toEntry
|
|
22
|
-
|
|
59
|
+
toEntry(entry, values) {
|
|
60
|
+
if (values === null)
|
|
61
|
+
return setUse(entry, 'jobs', undefined);
|
|
62
|
+
const next = {};
|
|
63
|
+
if (typeof values.retries === 'number' && values.retries !== JOBS_RETRIES_DEFAULT) {
|
|
64
|
+
next.retries = values.retries;
|
|
65
|
+
}
|
|
66
|
+
if (values.alertOwners === false)
|
|
67
|
+
next.alertOwners = false;
|
|
68
|
+
if (values.pauseOnFailures === true) {
|
|
69
|
+
next.pauseAfterFailures =
|
|
70
|
+
typeof values.pauseAfterFailures === 'number'
|
|
71
|
+
? values.pauseAfterFailures
|
|
72
|
+
: JOBS_PAUSE_AFTER_DEFAULT;
|
|
73
|
+
}
|
|
74
|
+
return setUse(entry, 'jobs', Object.keys(next).length === 0 ? true : next);
|
|
75
|
+
},
|
|
76
|
+
fromEntry(entry) {
|
|
77
|
+
const used = entry.uses?.jobs;
|
|
78
|
+
if (!used)
|
|
79
|
+
return null;
|
|
80
|
+
if (used === true)
|
|
81
|
+
return {};
|
|
82
|
+
const values = {};
|
|
83
|
+
if (used.retries !== undefined)
|
|
84
|
+
values.retries = used.retries;
|
|
85
|
+
if (used.alertOwners !== undefined)
|
|
86
|
+
values.alertOwners = used.alertOwners;
|
|
87
|
+
if (typeof used.pauseAfterFailures === 'number') {
|
|
88
|
+
values.pauseOnFailures = true;
|
|
89
|
+
values.pauseAfterFailures = used.pauseAfterFailures;
|
|
90
|
+
}
|
|
91
|
+
return values;
|
|
92
|
+
},
|
|
23
93
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AppModule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `uses.reporting` on a Worker app: the app sends its errors to the company's own reporting backend, and Boule
|
|
4
|
+
* shows them on the app's Errors page. Boule installs a site key on each copy of the app (`REPORTING_URL`,
|
|
5
|
+
* `REPORTING_SITE_KEY`; site = the app key, environment `production` for live and `stage` for the workspace);
|
|
6
|
+
* foundry only sets up the backend. One on/off module, no fields. A server app declares `uses.reporting` itself
|
|
7
|
+
* and gets its key from foundry.
|
|
8
|
+
*/
|
|
9
|
+
export declare const reporting: AppModule;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { setUse } from './entry-parts.js';
|
|
2
|
+
/**
|
|
3
|
+
* `uses.reporting` on a Worker app: the app sends its errors to the company's own reporting backend, and Boule
|
|
4
|
+
* shows them on the app's Errors page. Boule installs a site key on each copy of the app (`REPORTING_URL`,
|
|
5
|
+
* `REPORTING_SITE_KEY`; site = the app key, environment `production` for live and `stage` for the workspace);
|
|
6
|
+
* foundry only sets up the backend. One on/off module, no fields. A server app declares `uses.reporting` itself
|
|
7
|
+
* and gets its key from foundry.
|
|
8
|
+
*/
|
|
9
|
+
export const reporting = {
|
|
10
|
+
view: {
|
|
11
|
+
id: 'reporting',
|
|
12
|
+
name: 'Report errors',
|
|
13
|
+
line: 'We keep the errors the app runs into, so you can see what went wrong and when.',
|
|
14
|
+
category: 'Data',
|
|
15
|
+
icon: 'alert',
|
|
16
|
+
runtimes: ['worker'],
|
|
17
|
+
requires: [],
|
|
18
|
+
availability: 'ready',
|
|
19
|
+
fields: [],
|
|
20
|
+
adds: [
|
|
21
|
+
{
|
|
22
|
+
name: 'Errors',
|
|
23
|
+
hint: 'What went wrong in the app, how often and since when, on its Errors page. Kept in your own company only.',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
toEntry: (entry, values) => setUse(entry, 'reporting', values === null ? undefined : true),
|
|
28
|
+
fromEntry: (entry) => (entry.uses?.reporting === true ? {} : null),
|
|
29
|
+
};
|
package/dist/modules/sign-in.js
CHANGED
|
@@ -50,6 +50,14 @@ export const signIn = {
|
|
|
50
50
|
},
|
|
51
51
|
],
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
key: 'team',
|
|
55
|
+
label: 'Your team signs in too',
|
|
56
|
+
help: 'The people in your company sign in to the same app with their own company account.',
|
|
57
|
+
kind: 'switch',
|
|
58
|
+
showWhen: forCustomers,
|
|
59
|
+
runtimes: ['server'],
|
|
60
|
+
},
|
|
53
61
|
{
|
|
54
62
|
key: 'registration',
|
|
55
63
|
label: 'Who can make an account',
|
|
@@ -142,6 +150,9 @@ export const signIn = {
|
|
|
142
150
|
},
|
|
143
151
|
check(values) {
|
|
144
152
|
const customers = values.users === 'customers';
|
|
153
|
+
if (values.team === true && !customers) {
|
|
154
|
+
return problem('team', 'Your team already signs in when "Who signs in" is "Your team". Turn this on for an app your customers sign in to.');
|
|
155
|
+
}
|
|
145
156
|
if (!customers && values.registration === 'open') {
|
|
146
157
|
return problem('registration', 'Only customers can make their own account. For your team, choose "Only people you invite".');
|
|
147
158
|
}
|
|
@@ -195,9 +206,13 @@ export const signIn = {
|
|
|
195
206
|
}
|
|
196
207
|
// The parts other modules own (mcp, workspaces) and the operator's replacesBrokerEntry stay.
|
|
197
208
|
const kept = omit(entry.uses?.auth ?? {}, 'signIn', 'signIns', 'secondFactor', 'room');
|
|
209
|
+
// ADR 0003: customers with the team beside them is both audiences; one audience stays the single value.
|
|
210
|
+
const users = values.users === 'customers' && values.team === true
|
|
211
|
+
? ['customers', 'staff']
|
|
212
|
+
: values.users;
|
|
198
213
|
const auth = {
|
|
199
214
|
...kept,
|
|
200
|
-
users
|
|
215
|
+
users,
|
|
201
216
|
registration: values.registration,
|
|
202
217
|
};
|
|
203
218
|
const methods = methodsOf(values);
|
|
@@ -219,8 +234,11 @@ export const signIn = {
|
|
|
219
234
|
const auth = entry.uses?.auth;
|
|
220
235
|
if (!auth)
|
|
221
236
|
return null;
|
|
237
|
+
const listed = Array.isArray(auth.users) ? auth.users : [auth.users];
|
|
238
|
+
const audiences = ['customers', 'staff'].filter((a) => listed.includes(a));
|
|
222
239
|
const values = {
|
|
223
|
-
users:
|
|
240
|
+
users: audiences.includes('customers') ? 'customers' : 'staff',
|
|
241
|
+
...(audiences.length === 2 ? { team: true } : {}),
|
|
224
242
|
registration: auth.registration,
|
|
225
243
|
methods: auth.signIns ?? [auth.signIn ?? 'password'],
|
|
226
244
|
secondFactor: auth.secondFactor ?? 'off',
|
package/dist/modules/types.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface ModuleField {
|
|
|
34
34
|
pattern?: string;
|
|
35
35
|
/** The longest text, the most list items, or the largest number. */
|
|
36
36
|
max?: number;
|
|
37
|
+
/** For `number`: the smallest. Every `number` field is a whole number. */
|
|
38
|
+
min?: number;
|
|
37
39
|
/** Shown only while another field of the same module has this value. */
|
|
38
40
|
showWhen?: {
|
|
39
41
|
key: string;
|
|
@@ -40,6 +40,9 @@ export const workspaces = {
|
|
|
40
40
|
if (modules['sign-in']?.users !== 'customers') {
|
|
41
41
|
return problem(null, 'Workspaces are for your customers. In Sign-in, choose "Your customers" first.');
|
|
42
42
|
}
|
|
43
|
+
if (modules['sign-in']?.team === true) {
|
|
44
|
+
return problem(null, 'Workspaces are not ready yet for an app your team signs in to as well. In Sign-in, turn off "Your team signs in too".');
|
|
45
|
+
}
|
|
43
46
|
const roles = list(values, 'roles') ?? [];
|
|
44
47
|
const seen = new Set();
|
|
45
48
|
for (const [index, role] of roles.entries()) {
|