@wtfalch/foundry 0.60.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.
- package/dist/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/org.d.ts +163 -22
- package/dist/org.js +419 -47
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +41 -1
- package/dist/plan/blue-green.js +80 -2
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/desktop-callback.js +12 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +373 -42
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +191 -1
- package/dist/plan/executor.js +1505 -90
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +2 -2
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.js +12 -1
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +7 -2
- package/dist/plan/post-logout-uris.js +7 -1
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +10 -6
- package/dist/store/postgres.js +12 -2
- package/dist/store/schema.d.ts +37 -1
- package/dist/store/schema.js +4 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +18 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* English copy for every key the catalogue uses. Foundry owns it; Boule prefers its own `@wtfalch/i18n`
|
|
3
|
+
* catalogue for a key and falls back to this text. A key without text fails the catalogue test.
|
|
4
|
+
*/
|
|
5
|
+
const I = 'foundry.integration';
|
|
6
|
+
const S = 'foundry.system';
|
|
7
|
+
const INTEGRATION_COPY = {
|
|
8
|
+
'sign-in': {
|
|
9
|
+
name: 'Sign-in',
|
|
10
|
+
summary: 'People sign up and sign in to the app.',
|
|
11
|
+
ours: 'Run by us',
|
|
12
|
+
outputs: { issuer: 'The address that signs people in.', 'client-id': "The app's sign-in id." },
|
|
13
|
+
},
|
|
14
|
+
assistants: {
|
|
15
|
+
name: 'Help for AI assistants',
|
|
16
|
+
summary: 'Customers connect an AI assistant that acts as them.',
|
|
17
|
+
ours: 'Run by us',
|
|
18
|
+
outputs: { 'mcp-url': 'The address AI assistants connect to.' },
|
|
19
|
+
},
|
|
20
|
+
workspaces: {
|
|
21
|
+
name: 'Workspaces',
|
|
22
|
+
summary: 'Customers make shared workspaces and invite people in.',
|
|
23
|
+
ours: 'Run by us',
|
|
24
|
+
},
|
|
25
|
+
database: {
|
|
26
|
+
name: 'Database',
|
|
27
|
+
summary: "A database for the app's own records, with daily backups.",
|
|
28
|
+
ours: 'Run by us',
|
|
29
|
+
byo: 'Your own database',
|
|
30
|
+
meters: { storage: 'Storage' },
|
|
31
|
+
outputs: {
|
|
32
|
+
address: 'The address the app uses to reach its database.',
|
|
33
|
+
'owner-address': 'The address used to change the database layout.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
audit: {
|
|
37
|
+
name: 'Audit log',
|
|
38
|
+
summary: 'A record of what happened that nobody can quietly change, sealed and time stamped by the platform.',
|
|
39
|
+
ours: 'Run by us',
|
|
40
|
+
meters: { entries: 'Entries' },
|
|
41
|
+
outputs: {},
|
|
42
|
+
},
|
|
43
|
+
'signed-records': {
|
|
44
|
+
name: 'Signed records',
|
|
45
|
+
summary: 'Records the app signs so anyone can check they were not changed.',
|
|
46
|
+
ours: 'Run by us',
|
|
47
|
+
meters: { signatures: 'Signatures' },
|
|
48
|
+
outputs: { 'registrar-url': 'The address that signs records for the app.' },
|
|
49
|
+
},
|
|
50
|
+
files: {
|
|
51
|
+
name: 'Files',
|
|
52
|
+
summary: 'Uploads and documents kept in folders only this app reaches.',
|
|
53
|
+
ours: 'Run by us',
|
|
54
|
+
byo: 'Your own Cloudflare',
|
|
55
|
+
meters: { storage: 'Storage', transfer: 'Downloads' },
|
|
56
|
+
outputs: { address: "The address of the app's file folders." },
|
|
57
|
+
},
|
|
58
|
+
email: {
|
|
59
|
+
name: 'Email',
|
|
60
|
+
summary: "The app sends email from your company's own address.",
|
|
61
|
+
ours: 'Run by us',
|
|
62
|
+
meters: { messages: 'Messages sent' },
|
|
63
|
+
outputs: { from: 'The address the app sends from.' },
|
|
64
|
+
},
|
|
65
|
+
jobs: {
|
|
66
|
+
name: 'Jobs in the background',
|
|
67
|
+
summary: 'Tasks that run on a schedule, and try again if they fail.',
|
|
68
|
+
ours: 'Run by us',
|
|
69
|
+
meters: { runs: 'Runs' },
|
|
70
|
+
outputs: { url: 'The address the scheduler calls.' },
|
|
71
|
+
},
|
|
72
|
+
ai: {
|
|
73
|
+
name: 'AI',
|
|
74
|
+
summary: 'The app uses AI to do work, such as writing a summary.',
|
|
75
|
+
ours: 'Run by us',
|
|
76
|
+
byo: 'Your own AI account',
|
|
77
|
+
meters: { tokens: 'AI use' },
|
|
78
|
+
outputs: { address: 'The address the app calls for AI tasks.' },
|
|
79
|
+
},
|
|
80
|
+
organisations: {
|
|
81
|
+
name: 'Sign-in for organisations',
|
|
82
|
+
summary: 'Companies sign their own people in to the app.',
|
|
83
|
+
ours: 'Run by us',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const SHARED_OUTPUTS = {
|
|
87
|
+
app: { url: "The app's web address.", host: "The app's host name.", name: "The app's name." },
|
|
88
|
+
company: {
|
|
89
|
+
id: "The company's id.",
|
|
90
|
+
name: "The company's name.",
|
|
91
|
+
slug: "The company's short name.",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const SYSTEM_COPY = {
|
|
95
|
+
blank: { name: 'Start empty', summary: 'An app with nothing turned on. Add what you need.' },
|
|
96
|
+
'members-area': {
|
|
97
|
+
name: 'Members area',
|
|
98
|
+
summary: 'A place where your members sign in and keep their own records.',
|
|
99
|
+
screens: { members: 'Members' },
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
function build() {
|
|
103
|
+
const copy = {};
|
|
104
|
+
for (const [id, row] of Object.entries(INTEGRATION_COPY)) {
|
|
105
|
+
copy[`${I}.${id}.name`] = row.name;
|
|
106
|
+
copy[`${I}.${id}.summary`] = row.summary;
|
|
107
|
+
if (row.ours)
|
|
108
|
+
copy[`${I}.${id}.provider.ours`] = row.ours;
|
|
109
|
+
if (row.byo)
|
|
110
|
+
copy[`${I}.${id}.provider.byo`] = row.byo;
|
|
111
|
+
for (const [m, text] of Object.entries(row.meters ?? {}))
|
|
112
|
+
copy[`${I}.${id}.meter.${m}`] = text;
|
|
113
|
+
for (const [o, text] of Object.entries(row.outputs ?? {}))
|
|
114
|
+
copy[`${I}.${id}.output.${o}`] = text;
|
|
115
|
+
}
|
|
116
|
+
for (const [owner, outputs] of Object.entries(SHARED_OUTPUTS)) {
|
|
117
|
+
for (const [o, text] of Object.entries(outputs))
|
|
118
|
+
copy[`${I}.${owner}.output.${o}`] = text;
|
|
119
|
+
}
|
|
120
|
+
for (const [id, row] of Object.entries(SYSTEM_COPY)) {
|
|
121
|
+
copy[`${S}.${id}.name`] = row.name;
|
|
122
|
+
copy[`${S}.${id}.summary`] = row.summary;
|
|
123
|
+
for (const [sc, text] of Object.entries(row.screens ?? {})) {
|
|
124
|
+
copy[`${S}.${id}.screen.${sc}`] = text;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return copy;
|
|
128
|
+
}
|
|
129
|
+
export const CATALOGUE_EN = Object.freeze(build());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Catalogue } from './types.js';
|
|
2
|
+
export type * from './types.js';
|
|
3
|
+
export { CATALOGUE_EN } from './copy-en.js';
|
|
4
|
+
export { OUTPUTS, referenceProblem } from '../modules/outputs.js';
|
|
5
|
+
export { SYSTEMS } from './systems.js';
|
|
6
|
+
/** The English text for a copy key; the key itself when it has none (a test keeps that from happening). */
|
|
7
|
+
export declare function copyOf(key: string): string;
|
|
8
|
+
/** The whole catalogue, JSON-safe, for Boule to render. A fresh copy on every call. */
|
|
9
|
+
export declare function catalogue(): Catalogue;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CATALOGUE_EN } from './copy-en.js';
|
|
2
|
+
import { integrations, outputsOf } from './integrations.js';
|
|
3
|
+
import { SYSTEMS } from './systems.js';
|
|
4
|
+
export { CATALOGUE_EN } from './copy-en.js';
|
|
5
|
+
export { OUTPUTS, referenceProblem } from '../modules/outputs.js';
|
|
6
|
+
export { SYSTEMS } from './systems.js';
|
|
7
|
+
/** The English text for a copy key; the key itself when it has none (a test keeps that from happening). */
|
|
8
|
+
export function copyOf(key) {
|
|
9
|
+
return CATALOGUE_EN[key] ?? key;
|
|
10
|
+
}
|
|
11
|
+
/** The whole catalogue, JSON-safe, for Boule to render. A fresh copy on every call. */
|
|
12
|
+
export function catalogue() {
|
|
13
|
+
return structuredClone({
|
|
14
|
+
v: 1,
|
|
15
|
+
integrations: integrations(),
|
|
16
|
+
systems: [...SYSTEMS],
|
|
17
|
+
shared: { app: outputsOf('app'), company: outputsOf('company') },
|
|
18
|
+
runtimePrice: { worker: 'free', server: 'premium' },
|
|
19
|
+
copy: { ...CATALOGUE_EN },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Integration, IntegrationOutput } from './types.js';
|
|
2
|
+
export declare function outputsOf(owner: string): IntegrationOutput[];
|
|
3
|
+
/** Every integration, one per app module, in the order modules are applied. */
|
|
4
|
+
export declare function integrations(): Integration[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { MODULES } from '../modules/index.js';
|
|
2
|
+
import { OUTPUTS, SECRET_OUTPUTS } from '../modules/outputs.js';
|
|
3
|
+
const K = 'foundry.integration';
|
|
4
|
+
export function outputsOf(owner) {
|
|
5
|
+
return (OUTPUTS[owner] ?? []).map((id) => ({
|
|
6
|
+
id,
|
|
7
|
+
secret: SECRET_OUTPUTS.has(`${owner}.${id}`),
|
|
8
|
+
summaryKey: `${K}.${owner}.output.${id}`,
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
const ours = (integration) => ({
|
|
12
|
+
kind: 'ours',
|
|
13
|
+
nameKey: `${K}.${integration}.provider.ours`,
|
|
14
|
+
});
|
|
15
|
+
const byo = (integration) => ({
|
|
16
|
+
kind: 'byo',
|
|
17
|
+
nameKey: `${K}.${integration}.provider.byo`,
|
|
18
|
+
advanced: true,
|
|
19
|
+
});
|
|
20
|
+
const meter = (integration, id, unit) => ({
|
|
21
|
+
id,
|
|
22
|
+
nameKey: `${K}.${integration}.meter.${id}`,
|
|
23
|
+
unit,
|
|
24
|
+
});
|
|
25
|
+
const EXTRA = {
|
|
26
|
+
'sign-in': {
|
|
27
|
+
providers: [ours('sign-in')],
|
|
28
|
+
price: { class: 'free', meters: [] },
|
|
29
|
+
advanced: [],
|
|
30
|
+
},
|
|
31
|
+
assistants: {
|
|
32
|
+
providers: [ours('assistants')],
|
|
33
|
+
price: { class: 'free', meters: [] },
|
|
34
|
+
advanced: [],
|
|
35
|
+
},
|
|
36
|
+
workspaces: {
|
|
37
|
+
providers: [ours('workspaces')],
|
|
38
|
+
price: { class: 'free', meters: [] },
|
|
39
|
+
advanced: [],
|
|
40
|
+
},
|
|
41
|
+
database: {
|
|
42
|
+
providers: [ours('database'), byo('database')],
|
|
43
|
+
price: { class: 'usage', meters: [meter('database', 'storage', 'GB')] },
|
|
44
|
+
advanced: [],
|
|
45
|
+
},
|
|
46
|
+
'signed-records': {
|
|
47
|
+
providers: [ours('signed-records')],
|
|
48
|
+
price: { class: 'usage', meters: [meter('signed-records', 'signatures', 'signatures')] },
|
|
49
|
+
advanced: [],
|
|
50
|
+
},
|
|
51
|
+
files: {
|
|
52
|
+
providers: [ours('files'), byo('files')],
|
|
53
|
+
price: {
|
|
54
|
+
class: 'usage',
|
|
55
|
+
meters: [meter('files', 'storage', 'GB'), meter('files', 'transfer', 'GB')],
|
|
56
|
+
},
|
|
57
|
+
advanced: [],
|
|
58
|
+
},
|
|
59
|
+
email: {
|
|
60
|
+
providers: [ours('email')],
|
|
61
|
+
price: { class: 'usage', meters: [meter('email', 'messages', 'messages')] },
|
|
62
|
+
advanced: [],
|
|
63
|
+
},
|
|
64
|
+
jobs: {
|
|
65
|
+
providers: [ours('jobs')],
|
|
66
|
+
price: { class: 'usage', meters: [meter('jobs', 'runs', 'runs')] },
|
|
67
|
+
advanced: [],
|
|
68
|
+
},
|
|
69
|
+
ai: {
|
|
70
|
+
providers: [ours('ai'), byo('ai')],
|
|
71
|
+
price: { class: 'usage', meters: [meter('ai', 'tokens', 'tokens')] },
|
|
72
|
+
advanced: [],
|
|
73
|
+
},
|
|
74
|
+
audit: {
|
|
75
|
+
providers: [ours('audit')],
|
|
76
|
+
price: { class: 'usage', meters: [meter('audit', 'entries', 'entries')] },
|
|
77
|
+
advanced: [],
|
|
78
|
+
},
|
|
79
|
+
organisations: {
|
|
80
|
+
providers: [ours('organisations')],
|
|
81
|
+
price: { class: 'free', meters: [] },
|
|
82
|
+
advanced: [],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
/** Every integration, one per app module, in the order modules are applied. */
|
|
86
|
+
export function integrations() {
|
|
87
|
+
return MODULES.map(({ view }) => {
|
|
88
|
+
const extra = EXTRA[view.id];
|
|
89
|
+
if (!extra)
|
|
90
|
+
throw new Error(`The catalogue has no entry for the module "${view.id}".`);
|
|
91
|
+
return structuredClone({
|
|
92
|
+
id: view.id,
|
|
93
|
+
nameKey: `${K}.${view.id}.name`,
|
|
94
|
+
summaryKey: `${K}.${view.id}.summary`,
|
|
95
|
+
category: view.category,
|
|
96
|
+
icon: view.icon,
|
|
97
|
+
runtimes: view.runtimes,
|
|
98
|
+
requires: view.requires,
|
|
99
|
+
availability: view.availability,
|
|
100
|
+
...extra,
|
|
101
|
+
outputs: outputsOf(view.id),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { System } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The first systems, as recipes on top of the integrations (estate#219). A system names the integrations it
|
|
4
|
+
* turns on with the values it starts from, the questions the quickstart asks, and the Boule screens it adds.
|
|
5
|
+
* More systems come in waves.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SYSTEMS: readonly System[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const S = 'foundry.system';
|
|
2
|
+
/**
|
|
3
|
+
* The first systems, as recipes on top of the integrations (estate#219). A system names the integrations it
|
|
4
|
+
* turns on with the values it starts from, the questions the quickstart asks, and the Boule screens it adds.
|
|
5
|
+
* More systems come in waves.
|
|
6
|
+
*/
|
|
7
|
+
export const SYSTEMS = [
|
|
8
|
+
{
|
|
9
|
+
id: 'blank',
|
|
10
|
+
nameKey: `${S}.blank.name`,
|
|
11
|
+
summaryKey: `${S}.blank.summary`,
|
|
12
|
+
icon: 'app',
|
|
13
|
+
runtime: 'worker',
|
|
14
|
+
presets: [],
|
|
15
|
+
asks: [],
|
|
16
|
+
screens: [],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'members-area',
|
|
20
|
+
nameKey: `${S}.members-area.name`,
|
|
21
|
+
summaryKey: `${S}.members-area.summary`,
|
|
22
|
+
icon: 'users',
|
|
23
|
+
runtime: 'worker',
|
|
24
|
+
presets: [
|
|
25
|
+
{ integration: 'sign-in', values: {} },
|
|
26
|
+
{ integration: 'database', values: {} },
|
|
27
|
+
],
|
|
28
|
+
asks: [],
|
|
29
|
+
screens: [{ id: 'members', nameKey: `${S}.members-area.screen.members` }],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ModuleCategory, ModuleRuntime } from '../modules/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The catalogue contract (`@wtfalch/foundry/catalogue`). Boule renders every integration and system from it,
|
|
4
|
+
* with no code per integration. Everything is JSON-safe.
|
|
5
|
+
*
|
|
6
|
+
* Copy: each entry carries `@wtfalch/i18n` copy keys. `CATALOGUE_EN` holds the English text for every key
|
|
7
|
+
* (foundry owns this copy); `copyOf` turns a key into its English fallback. Boule prefers its own catalogue
|
|
8
|
+
* for a key and falls back to English.
|
|
9
|
+
*/
|
|
10
|
+
/** How a price shows: free with the app, a Premium part, or priced by use. */
|
|
11
|
+
export type PriceClass = 'free' | 'premium' | 'usage';
|
|
12
|
+
/** Who runs the integration. `ours`: us, per company (the default). `byo`: the company's own account. */
|
|
13
|
+
export type ProviderKind = 'ours' | 'byo';
|
|
14
|
+
export interface CopyKeys {
|
|
15
|
+
nameKey: string;
|
|
16
|
+
summaryKey: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IntegrationProvider {
|
|
19
|
+
kind: ProviderKind;
|
|
20
|
+
/** Copy key for the provider's name, such as "Run by us" or "Your own Cloudflare". */
|
|
21
|
+
nameKey: string;
|
|
22
|
+
/** Present only for `byo`: an Advanced option. */
|
|
23
|
+
advanced?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** What a meter counts. The meter ids are the ones the usage tail reports. */
|
|
26
|
+
export interface PriceMeter {
|
|
27
|
+
id: string;
|
|
28
|
+
/** Copy key for the meter's name, such as "Storage". */
|
|
29
|
+
nameKey: string;
|
|
30
|
+
unit: string;
|
|
31
|
+
}
|
|
32
|
+
export interface IntegrationPrice {
|
|
33
|
+
class: PriceClass;
|
|
34
|
+
meters: PriceMeter[];
|
|
35
|
+
}
|
|
36
|
+
/** A value an app can read with a `reference` in `company-app.json`: `<integration id>.<output id>`. */
|
|
37
|
+
export interface IntegrationOutput {
|
|
38
|
+
id: string;
|
|
39
|
+
/** Always held as a secret by whoever reads it. */
|
|
40
|
+
secret: boolean;
|
|
41
|
+
/** Copy key for one plain sentence on what it is. */
|
|
42
|
+
summaryKey: string;
|
|
43
|
+
}
|
|
44
|
+
/** One option under "Advanced", beyond bring-your-own. */
|
|
45
|
+
export interface AdvancedOption {
|
|
46
|
+
id: string;
|
|
47
|
+
nameKey: string;
|
|
48
|
+
summaryKey: string;
|
|
49
|
+
}
|
|
50
|
+
export interface Integration extends CopyKeys {
|
|
51
|
+
/** The id of the app module that turns it on. */
|
|
52
|
+
id: string;
|
|
53
|
+
category: ModuleCategory;
|
|
54
|
+
icon: string;
|
|
55
|
+
runtimes: ModuleRuntime[];
|
|
56
|
+
requires: string[];
|
|
57
|
+
availability: 'ready' | 'soon';
|
|
58
|
+
/** The first is the default and is always `ours`. */
|
|
59
|
+
providers: IntegrationProvider[];
|
|
60
|
+
price: IntegrationPrice;
|
|
61
|
+
outputs: IntegrationOutput[];
|
|
62
|
+
advanced: AdvancedOption[];
|
|
63
|
+
}
|
|
64
|
+
/** An integration a system turns on, with the module values it starts from. */
|
|
65
|
+
export interface SystemPreset {
|
|
66
|
+
integration: string;
|
|
67
|
+
/** Module values the system sets. The company may change them unless `locked`. */
|
|
68
|
+
values: Record<string, string | number | boolean | string[]>;
|
|
69
|
+
locked?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** A question the quickstart asks before it builds the system. */
|
|
72
|
+
export interface SystemAsk {
|
|
73
|
+
key: string;
|
|
74
|
+
nameKey: string;
|
|
75
|
+
helpKey?: string;
|
|
76
|
+
required: boolean;
|
|
77
|
+
/** Where the answer goes: a field of a preset integration. */
|
|
78
|
+
into?: {
|
|
79
|
+
integration: string;
|
|
80
|
+
field: string;
|
|
81
|
+
};
|
|
82
|
+
default?: string;
|
|
83
|
+
}
|
|
84
|
+
/** A screen of Boule that belongs to the system once installed. */
|
|
85
|
+
export interface SystemScreen {
|
|
86
|
+
id: string;
|
|
87
|
+
nameKey: string;
|
|
88
|
+
}
|
|
89
|
+
export interface System extends CopyKeys {
|
|
90
|
+
id: string;
|
|
91
|
+
icon: string;
|
|
92
|
+
runtime: ModuleRuntime;
|
|
93
|
+
presets: SystemPreset[];
|
|
94
|
+
asks: SystemAsk[];
|
|
95
|
+
screens: SystemScreen[];
|
|
96
|
+
}
|
|
97
|
+
export interface Catalogue {
|
|
98
|
+
v: 1;
|
|
99
|
+
integrations: Integration[];
|
|
100
|
+
systems: System[];
|
|
101
|
+
/** Outputs every app can read: `app.<id>` and `company.<id>`. */
|
|
102
|
+
shared: {
|
|
103
|
+
app: IntegrationOutput[];
|
|
104
|
+
company: IntegrationOutput[];
|
|
105
|
+
};
|
|
106
|
+
/** Price class of running on a server (a container) or as a Worker. */
|
|
107
|
+
runtimePrice: Record<ModuleRuntime, PriceClass>;
|
|
108
|
+
/** Copy key -> English text, for every key above. */
|
|
109
|
+
copy: Record<string, string>;
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape rules of a hosting entry that both the entry's validation (`org.ts`, `worker-app-config.ts`) and the
|
|
3
|
+
* app modules (`modules/`) read. Pure: no imports, so `@wtfalch/foundry/modules` can use them in a browser.
|
|
4
|
+
*/
|
|
5
|
+
/** The MCP endpoint path: absolute, plain characters, no dot segments, no query. */
|
|
6
|
+
export declare const MCP_PATH: RegExp;
|
|
7
|
+
/** `uses.auth.mcp.onboardingPath` and `uses.auth.workspaces.invitePath`: their longest value. */
|
|
8
|
+
export declare const ONBOARDING_PATH_MAX_LENGTH = 200;
|
|
9
|
+
/**
|
|
10
|
+
* A page path on the app's own host: starts with one `/`, no `..`, `?`, `#`, `\`, spaces or control characters,
|
|
11
|
+
* at most {@link ONBOARDING_PATH_MAX_LENGTH} characters.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isPagePath(value: unknown): value is string;
|
|
14
|
+
/** The most roles an entry may offer, as the broker allows. */
|
|
15
|
+
export declare const WORKSPACE_ROLES_MAX = 20;
|
|
16
|
+
/** A role name is the role's key in the broker's store. */
|
|
17
|
+
export declare const WORKSPACE_ROLE: RegExp;
|
|
18
|
+
/** Longest value of each `uses.auth.room` text field. */
|
|
19
|
+
export declare const ROOM_TEXT_MAX: {
|
|
20
|
+
readonly name: 40;
|
|
21
|
+
readonly headline: 120;
|
|
22
|
+
readonly line: 200;
|
|
23
|
+
readonly title: 60;
|
|
24
|
+
readonly description: 200;
|
|
25
|
+
};
|
|
26
|
+
export declare const ROOM_STEPS_MIN = 2;
|
|
27
|
+
export declare const ROOM_STEPS_MAX = 9;
|
|
28
|
+
export declare const ROOM_UNSAFE_TEXT: RegExp;
|
|
29
|
+
/** What an entry's ai key may hold: the most the org's own key holds. */
|
|
30
|
+
export declare const AI_ENTRY_PERMISSIONS: readonly ["ai.offerings:invoke", "ai.runs:create"];
|
|
31
|
+
/** `uses.ai.webhookPath`. */
|
|
32
|
+
export declare const AI_WEBHOOK_PATH: RegExp;
|
|
33
|
+
/** What an entry's files key may hold: read and write on its collections, and the most the org's own key holds besides minting. */
|
|
34
|
+
export declare const FILES_ENTRY_PERMISSIONS: readonly ["files.collections:read", "files.files:read", "files.files:download", "files.files:upload", "files.files:replace", "files.files:delete"];
|
|
35
|
+
/** A `uses.files.collections` name. */
|
|
36
|
+
export declare const FILES_COLLECTION_NAME: RegExp;
|
|
37
|
+
/** The secret that carries a `keysCustody` source: the custody Worker credential. */
|
|
38
|
+
export declare const WORKER_APP_KEYS_SECRET = "KEYS_CREDENTIAL";
|
|
39
|
+
/**
|
|
40
|
+
* Names foundry writes into a Worker app's environment itself, so a
|
|
41
|
+
* declaration may not use them as a binding, secret or var.
|
|
42
|
+
*/
|
|
43
|
+
export declare const WORKER_APP_RESERVED_NAMES: RegExp;
|
|
44
|
+
/** A var name that looks like a secret's. */
|
|
45
|
+
export declare const SECRET_LIKE_NAME: RegExp;
|
|
46
|
+
/** A Worker app binding, secret or var name. */
|
|
47
|
+
export declare const WORKER_APP_ENV_NAME: RegExp;
|
|
48
|
+
/** The most tables `database.appendOnly` may name, and the shape of one name (`table` or `schema.table`). */
|
|
49
|
+
export declare const WORKER_APP_APPEND_ONLY_MAX = 100;
|
|
50
|
+
export declare const WORKER_APP_TABLE_NAME: RegExp;
|
|
51
|
+
/** The most functions `database.grants` may name, and the shape of one signature. It is spliced into SQL, so nothing else passes. */
|
|
52
|
+
export declare const WORKER_APP_GRANTS_MAX = 100;
|
|
53
|
+
export declare const WORKER_APP_FUNCTION_SIGNATURE: RegExp;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape rules of a hosting entry that both the entry's validation (`org.ts`, `worker-app-config.ts`) and the
|
|
3
|
+
* app modules (`modules/`) read. Pure: no imports, so `@wtfalch/foundry/modules` can use them in a browser.
|
|
4
|
+
*/
|
|
5
|
+
/** The MCP endpoint path: absolute, plain characters, no dot segments, no query. */
|
|
6
|
+
export const MCP_PATH = /^\/[A-Za-z0-9_~-]+(\/[A-Za-z0-9_~-]+)*$|^\/$/;
|
|
7
|
+
/** `uses.auth.mcp.onboardingPath` and `uses.auth.workspaces.invitePath`: their longest value. */
|
|
8
|
+
export const ONBOARDING_PATH_MAX_LENGTH = 200;
|
|
9
|
+
/**
|
|
10
|
+
* A page path on the app's own host: starts with one `/`, no `..`, `?`, `#`, `\`, spaces or control characters,
|
|
11
|
+
* at most {@link ONBOARDING_PATH_MAX_LENGTH} characters.
|
|
12
|
+
*/
|
|
13
|
+
export function isPagePath(value) {
|
|
14
|
+
return (typeof value === 'string' &&
|
|
15
|
+
value.startsWith('/') &&
|
|
16
|
+
!value.startsWith('//') &&
|
|
17
|
+
!value.includes('..') &&
|
|
18
|
+
!value.includes('?') &&
|
|
19
|
+
!value.includes('#') &&
|
|
20
|
+
!value.includes('\\') &&
|
|
21
|
+
!/[\s\p{Cc}]/u.test(value) &&
|
|
22
|
+
value.length <= ONBOARDING_PATH_MAX_LENGTH);
|
|
23
|
+
}
|
|
24
|
+
/** The most roles an entry may offer, as the broker allows. */
|
|
25
|
+
export const WORKSPACE_ROLES_MAX = 20;
|
|
26
|
+
/** A role name is the role's key in the broker's store. */
|
|
27
|
+
export const WORKSPACE_ROLE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
28
|
+
/** Longest value of each `uses.auth.room` text field. */
|
|
29
|
+
export const ROOM_TEXT_MAX = {
|
|
30
|
+
name: 40,
|
|
31
|
+
headline: 120,
|
|
32
|
+
line: 200,
|
|
33
|
+
title: 60,
|
|
34
|
+
description: 200,
|
|
35
|
+
};
|
|
36
|
+
export const ROOM_STEPS_MIN = 2;
|
|
37
|
+
export const ROOM_STEPS_MAX = 9;
|
|
38
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: the point is to refuse them
|
|
39
|
+
export const ROOM_UNSAFE_TEXT = /[\u0000-\u001f\u007f<>]/;
|
|
40
|
+
/** What an entry's ai key may hold: the most the org's own key holds. */
|
|
41
|
+
export const AI_ENTRY_PERMISSIONS = ['ai.offerings:invoke', 'ai.runs:create'];
|
|
42
|
+
/** `uses.ai.webhookPath`. */
|
|
43
|
+
export const AI_WEBHOOK_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,199}$/;
|
|
44
|
+
/** What an entry's files key may hold: read and write on its collections, and the most the org's own key holds besides minting. */
|
|
45
|
+
export const FILES_ENTRY_PERMISSIONS = [
|
|
46
|
+
'files.collections:read',
|
|
47
|
+
'files.files:read',
|
|
48
|
+
'files.files:download',
|
|
49
|
+
'files.files:upload',
|
|
50
|
+
'files.files:replace',
|
|
51
|
+
'files.files:delete',
|
|
52
|
+
];
|
|
53
|
+
/** A `uses.files.collections` name. */
|
|
54
|
+
export const FILES_COLLECTION_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
55
|
+
/** The secret that carries a `keysCustody` source: the custody Worker credential. */
|
|
56
|
+
export const WORKER_APP_KEYS_SECRET = 'KEYS_CREDENTIAL';
|
|
57
|
+
/**
|
|
58
|
+
* Names foundry writes into a Worker app's environment itself, so a
|
|
59
|
+
* declaration may not use them as a binding, secret or var.
|
|
60
|
+
*/
|
|
61
|
+
export const WORKER_APP_RESERVED_NAMES = /^(AUTH_.*|KEYS_.*|MCP_.*|HYPERDRIVE)$/;
|
|
62
|
+
/** A var name that looks like a secret's. */
|
|
63
|
+
export const SECRET_LIKE_NAME = /_(KEY|SECRET|TOKEN|PASSWORD)$/;
|
|
64
|
+
/** A Worker app binding, secret or var name. */
|
|
65
|
+
export const WORKER_APP_ENV_NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
66
|
+
/** The most tables `database.appendOnly` may name, and the shape of one name (`table` or `schema.table`). */
|
|
67
|
+
export const WORKER_APP_APPEND_ONLY_MAX = 100;
|
|
68
|
+
export const WORKER_APP_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,62}(\.[A-Za-z_][A-Za-z0-9_]{0,62})?$/;
|
|
69
|
+
/** The most functions `database.grants` may name, and the shape of one signature. It is spliced into SQL, so nothing else passes. */
|
|
70
|
+
export const WORKER_APP_GRANTS_MAX = 100;
|
|
71
|
+
export const WORKER_APP_FUNCTION_SIGNATURE = /^[A-Za-z_][A-Za-z0-9_]{0,62}(\.[A-Za-z_][A-Za-z0-9_]{0,62})?\(([a-z][a-z0-9_]*( [a-z][a-z0-9_]*)?(, [a-z][a-z0-9_]*( [a-z][a-z0-9_]*)?)*)?\)$/;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type HostedLabelRefusal = {
|
|
2
|
+
code: 'invalid' | 'barred' | 'locked';
|
|
3
|
+
message: string;
|
|
4
|
+
};
|
|
5
|
+
/** The first-label prefix of every workspace host (`work-<slug>-<key>`). */
|
|
6
|
+
export declare const WORKSPACE_HOST_PREFIX = "work-";
|
|
7
|
+
/** The first-label prefix of every test inbox host (`inbox-<slug>`). */
|
|
8
|
+
export declare const INBOX_HOST_PREFIX = "inbox-";
|
|
9
|
+
/**
|
|
10
|
+
* Why `label` may not be the first label of an app address (`<label>.companyincluded.app`), or `null`
|
|
11
|
+
* when it may. The message is for a person: it never repeats a refused word back.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hostedLabelRefusal(label: string): HostedLabelRefusal | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { checkName } from '@wtfalch/auth/names';
|
|
2
|
+
import { FOUNDRY_SERVICE_LABELS } from './service-labels.js';
|
|
3
|
+
import { RESERVED_SLUGS, RESERVED_SLUG_PREFIX, hasBarredSlugSuffix } from './slug.js';
|
|
4
|
+
/** The first-label prefix of every workspace host (`work-<slug>-<key>`). */
|
|
5
|
+
export const WORKSPACE_HOST_PREFIX = 'work-';
|
|
6
|
+
/** The first-label prefix of every test inbox host (`inbox-<slug>`). */
|
|
7
|
+
export const INBOX_HOST_PREFIX = 'inbox-';
|
|
8
|
+
const DNS_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
9
|
+
/** Words that make a name look like a bank, a sign-in page or a big brand. Matched as whole dash-separated words. */
|
|
10
|
+
const LOOKALIKE_WORDS = [
|
|
11
|
+
'bank',
|
|
12
|
+
'login',
|
|
13
|
+
'signin',
|
|
14
|
+
'sign-in',
|
|
15
|
+
'password',
|
|
16
|
+
'vipps',
|
|
17
|
+
'bankid',
|
|
18
|
+
'paypal',
|
|
19
|
+
'apple',
|
|
20
|
+
'google',
|
|
21
|
+
'microsoft',
|
|
22
|
+
'support',
|
|
23
|
+
'secure',
|
|
24
|
+
'verify',
|
|
25
|
+
'wallet',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* The auth name check with its length and letter rules loosened: only its profanity verdict (`disallowed`) applies
|
|
29
|
+
* to a label. Its `reserved` verdict is not used: `@wtfalch/auth/names` merges the generic reserved-usernames list
|
|
30
|
+
* (`shop`, `blog`, ...) with the system names and does not export them apart.
|
|
31
|
+
*/
|
|
32
|
+
const ANY_TEXT = { minLength: 0, maxLength: Number.MAX_SAFE_INTEGER, pattern: /^/ };
|
|
33
|
+
function containsWord(label, word) {
|
|
34
|
+
return `-${label}-`.includes(`-${word}-`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Why `label` may not be the first label of an app address (`<label>.companyincluded.app`), or `null`
|
|
38
|
+
* when it may. The message is for a person: it never repeats a refused word back.
|
|
39
|
+
*/
|
|
40
|
+
export function hostedLabelRefusal(label) {
|
|
41
|
+
if (!DNS_LABEL.test(label) || label.slice(2, 4) === '--') {
|
|
42
|
+
return {
|
|
43
|
+
code: 'invalid',
|
|
44
|
+
message: 'An app address can use only lowercase letters, numbers and dashes, from 1 to 63 characters. It cannot start or end with a dash, or have two dashes in a row after the second character.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (LOOKALIKE_WORDS.some((word) => containsWord(label, word))) {
|
|
48
|
+
return {
|
|
49
|
+
code: 'barred',
|
|
50
|
+
message: 'That address could be mistaken for a bank, a sign-in page or a well-known company. Choose a name that is clearly your own.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (FOUNDRY_SERVICE_LABELS.has(label) ||
|
|
54
|
+
RESERVED_SLUGS.has(label) ||
|
|
55
|
+
label.startsWith(RESERVED_SLUG_PREFIX)) {
|
|
56
|
+
return {
|
|
57
|
+
code: 'locked',
|
|
58
|
+
message: 'That address is used by the platform itself. Choose a different one.',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (label.startsWith(WORKSPACE_HOST_PREFIX) || label.startsWith(INBOX_HOST_PREFIX)) {
|
|
62
|
+
return {
|
|
63
|
+
code: 'barred',
|
|
64
|
+
message: 'App addresses cannot start with work- or inbox-. Those names are kept for private workspaces and test inboxes. Choose a different one.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (hasBarredSlugSuffix(label)) {
|
|
68
|
+
return {
|
|
69
|
+
code: 'barred',
|
|
70
|
+
message: 'That address ends in a word the platform keeps for its own addresses, such as stage, calls or auth. Choose a different ending.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const verdict = checkName(label, ANY_TEXT);
|
|
74
|
+
if (!verdict.ok && verdict.reason === 'disallowed') {
|
|
75
|
+
return {
|
|
76
|
+
code: 'barred',
|
|
77
|
+
message: 'That address is not allowed. Choose a different one.',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|