@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.
- 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 +47 -40
- package/dist/plan/worker-app-database.js +150 -152
- package/dist/plan/worker-app.d.ts +6 -65
- package/dist/plan/worker-app.js +29 -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 +6 -10
- package/dist/worker-app-config.js +22 -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,49 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* The registry of app ledgers (foundry#929). An app is registered by declaring `uses.audit`; there is no other
|
|
4
|
+
* list. Two views of the same fact:
|
|
5
|
+
*
|
|
6
|
+
* - {@link declaredAppLedgers}: every ledger the declared org asks for.
|
|
7
|
+
* - {@link registeredAppLedgers}: the ones the `audit:ledger` step has applied (`plan/entry-audit.ts` records
|
|
8
|
+
* `audit_ledger.<entry>`), which is when the app's database has the tables the sealer reads. Only these are
|
|
9
|
+
* sealed.
|
|
10
|
+
*
|
|
11
|
+
* Retention is a record of the policy, not an action: nothing here or in the sealing service prunes, deletes or
|
|
12
|
+
* rewrites a ledger (append-only; the audit package has no prune path either).
|
|
13
|
+
*/
|
|
14
|
+
/** The retention a ledger is kept under. */
|
|
15
|
+
export interface LedgerRetention {
|
|
16
|
+
/** The plan's period in months, or `null` when the plan keeps it for as long as the company exists. */
|
|
17
|
+
months: number | null;
|
|
18
|
+
/** Under a legal hold the ledger is kept forever whatever `months` says. */
|
|
19
|
+
legalHold: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface AppLedger {
|
|
22
|
+
orgId: string;
|
|
23
|
+
orgSlug: string;
|
|
24
|
+
entryKey: string;
|
|
25
|
+
/** The ledger's name in every checkpoint it signs. Stable: it is part of what a checkpoint's hash covers. */
|
|
26
|
+
ledger: string;
|
|
27
|
+
retention: LedgerRetention;
|
|
28
|
+
/** The database role the sealing service connects as (`audit/sealer-role.ts`), once the apply recorded it. */
|
|
29
|
+
sealerRole?: string;
|
|
30
|
+
}
|
|
31
|
+
/** What the host knows about retention. The plan's period is the host's (billing) to say; foundry holds no default. */
|
|
32
|
+
export interface AuditPolicy {
|
|
33
|
+
/** The plan's period in months for this org, or `null` for no end. */
|
|
34
|
+
retentionMonths(org: Pick<DeclaredOrg, 'id' | 'slug'>): number | null;
|
|
35
|
+
/** Org ids under a legal hold. */
|
|
36
|
+
legalHold?: ReadonlySet<string>;
|
|
37
|
+
}
|
|
38
|
+
/** The held name of a ledger: `<org slug>-<entry key>`. */
|
|
39
|
+
export declare const appLedgerName: (org: Pick<DeclaredOrg, "slug">, entryKey: string) => string;
|
|
40
|
+
/** `audit_ledger.<entry>`: what the registration step recorded (the ledger name), cleared never. */
|
|
41
|
+
export declare const auditLedgerScopeKey: (entryKey: string) => string;
|
|
42
|
+
export declare const AUDIT_LEDGER_PREFIX = "audit_ledger.";
|
|
43
|
+
export declare function retentionOf(org: Pick<DeclaredOrg, 'id' | 'slug'>, policy: AuditPolicy): LedgerRetention;
|
|
44
|
+
/** Plain words on how long a ledger is kept, for Boule to show. */
|
|
45
|
+
export declare function describeRetention(retention: LedgerRetention): string;
|
|
46
|
+
/** Every ledger the declared org asks for with `uses.audit`. */
|
|
47
|
+
export declare function declaredAppLedgers(org: DeclaredOrg, policy: AuditPolicy): AppLedger[];
|
|
48
|
+
/** The declared ledgers whose registration step has applied. An entry that dropped `uses.audit` is no longer sealed. */
|
|
49
|
+
export declare function registeredAppLedgers(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, policy: AuditPolicy): AppLedger[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { auditSealerScopeKey } from './sealer-role.js';
|
|
2
|
+
/** The held name of a ledger: `<org slug>-<entry key>`. */
|
|
3
|
+
export const appLedgerName = (org, entryKey) => `${org.slug}-${entryKey}`;
|
|
4
|
+
/** `audit_ledger.<entry>`: what the registration step recorded (the ledger name), cleared never. */
|
|
5
|
+
export const auditLedgerScopeKey = (entryKey) => `audit_ledger.${entryKey}`;
|
|
6
|
+
export const AUDIT_LEDGER_PREFIX = 'audit_ledger.';
|
|
7
|
+
export function retentionOf(org, policy) {
|
|
8
|
+
const legalHold = policy.legalHold?.has(org.id) === true;
|
|
9
|
+
return { months: legalHold ? null : policy.retentionMonths(org), legalHold };
|
|
10
|
+
}
|
|
11
|
+
/** Plain words on how long a ledger is kept, for Boule to show. */
|
|
12
|
+
export function describeRetention(retention) {
|
|
13
|
+
if (retention.legalHold)
|
|
14
|
+
return 'Kept without an end date while a legal hold is in place.';
|
|
15
|
+
if (retention.months === null)
|
|
16
|
+
return 'Kept for as long as your company has the app.';
|
|
17
|
+
return `Kept for ${retention.months} months, as your plan says.`;
|
|
18
|
+
}
|
|
19
|
+
function ledgerOf(org, entryKey, policy) {
|
|
20
|
+
return {
|
|
21
|
+
orgId: org.id,
|
|
22
|
+
orgSlug: org.slug,
|
|
23
|
+
entryKey,
|
|
24
|
+
ledger: appLedgerName(org, entryKey),
|
|
25
|
+
retention: retentionOf(org, policy),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Every ledger the declared org asks for with `uses.audit`. */
|
|
29
|
+
export function declaredAppLedgers(org, policy) {
|
|
30
|
+
if (!org.hosting.enabled)
|
|
31
|
+
return [];
|
|
32
|
+
return org.hosting.entries
|
|
33
|
+
.filter((entry) => entry.uses?.audit === true)
|
|
34
|
+
.map((entry) => ledgerOf(org, entry.key, policy));
|
|
35
|
+
}
|
|
36
|
+
/** The declared ledgers whose registration step has applied. An entry that dropped `uses.audit` is no longer sealed. */
|
|
37
|
+
export function registeredAppLedgers(org, recordedIds, policy) {
|
|
38
|
+
return declaredAppLedgers(org, policy)
|
|
39
|
+
.filter((ledger) => recordedIds[auditLedgerScopeKey(ledger.entryKey)] === ledger.ledger)
|
|
40
|
+
.map((ledger) => {
|
|
41
|
+
const sealerRole = recordedIds[auditSealerScopeKey(ledger.entryKey)];
|
|
42
|
+
return sealerRole ? { ...ledger, sealerRole } : ledger;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CheckpointSigner } from '@wtfalch/audit-sealing';
|
|
2
|
+
import { type LedgerConnector } from './connector.js';
|
|
3
|
+
import type { AppLedger } from './registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* The sealing and anchoring tick (foundry#929, audit#56). The host calls {@link sealLedgers} on
|
|
6
|
+
* {@link AUDIT_SEAL_EVERY_MINUTES}; one call seals every registered app ledger with the platform signer and has
|
|
7
|
+
* the timestamp authority stamp each new checkpoint. It is idempotent: a ledger with no new rows seals nothing,
|
|
8
|
+
* and `anchorCheckpoints` anchors only checkpoints the authority has not stamped yet.
|
|
9
|
+
*
|
|
10
|
+
* One ledger failing never stops the others. Every ledger gets a result that says what happened and, when
|
|
11
|
+
* nothing did, why. Nothing here deletes, updates or prunes a row.
|
|
12
|
+
*/
|
|
13
|
+
/** How often the host runs the tick. */
|
|
14
|
+
export declare const AUDIT_SEAL_EVERY_MINUTES = 15;
|
|
15
|
+
/** A timestamp authority (RFC 3161). Host-chosen: foundry names none. */
|
|
16
|
+
export interface TimestampAuthority {
|
|
17
|
+
tsaUrl: string;
|
|
18
|
+
/** A name for the authority; one anchor per checkpoint and name. */
|
|
19
|
+
provider: string;
|
|
20
|
+
}
|
|
21
|
+
export type SealOutcome = 'sealed' | 'unchanged' | 'skipped' | 'failed';
|
|
22
|
+
export interface SealResult {
|
|
23
|
+
ledger: string;
|
|
24
|
+
orgId: string;
|
|
25
|
+
entryKey: string;
|
|
26
|
+
outcome: SealOutcome;
|
|
27
|
+
/** The new checkpoint's tree size, when one was sealed. */
|
|
28
|
+
treeSize?: number;
|
|
29
|
+
/** How many checkpoints the authority stamped in this tick. */
|
|
30
|
+
anchored: number;
|
|
31
|
+
/** Plain words on why a step did not happen. Present whenever `outcome` is not `sealed`, or anchoring did not run. */
|
|
32
|
+
reason?: string;
|
|
33
|
+
at: string;
|
|
34
|
+
}
|
|
35
|
+
export interface SealDeps {
|
|
36
|
+
ledgers: readonly AppLedger[];
|
|
37
|
+
connector: LedgerConnector;
|
|
38
|
+
signer: CheckpointSigner;
|
|
39
|
+
/** Null when the host picked none: the ledger is sealed and not anchored, and each result says so. */
|
|
40
|
+
authority: TimestampAuthority | null;
|
|
41
|
+
fetch?: typeof fetch;
|
|
42
|
+
now?: () => Date;
|
|
43
|
+
/** Called with each result as it is known (a log line, a status row). A throwing callback is ignored. */
|
|
44
|
+
onResult?: (result: SealResult) => void;
|
|
45
|
+
}
|
|
46
|
+
/** A failure in words that carry no address, key or credential. */
|
|
47
|
+
export declare function plainFailure(error: unknown): string;
|
|
48
|
+
/** Seals and anchors each ledger in turn. Never throws for one ledger's failure. */
|
|
49
|
+
export declare function sealLedgers(deps: SealDeps): Promise<SealResult[]>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { anchorCheckpoints, sealCheckpoint } from '@wtfalch/audit-sealing';
|
|
2
|
+
import { isUnavailable } from './connector.js';
|
|
3
|
+
/**
|
|
4
|
+
* The sealing and anchoring tick (foundry#929, audit#56). The host calls {@link sealLedgers} on
|
|
5
|
+
* {@link AUDIT_SEAL_EVERY_MINUTES}; one call seals every registered app ledger with the platform signer and has
|
|
6
|
+
* the timestamp authority stamp each new checkpoint. It is idempotent: a ledger with no new rows seals nothing,
|
|
7
|
+
* and `anchorCheckpoints` anchors only checkpoints the authority has not stamped yet.
|
|
8
|
+
*
|
|
9
|
+
* One ledger failing never stops the others. Every ledger gets a result that says what happened and, when
|
|
10
|
+
* nothing did, why. Nothing here deletes, updates or prunes a row.
|
|
11
|
+
*/
|
|
12
|
+
/** How often the host runs the tick. */
|
|
13
|
+
export const AUDIT_SEAL_EVERY_MINUTES = 15;
|
|
14
|
+
const MAX_REASON = 300;
|
|
15
|
+
/** A failure in words that carry no address, key or credential. */
|
|
16
|
+
export function plainFailure(error) {
|
|
17
|
+
const name = error instanceof Error ? error.name : 'Error';
|
|
18
|
+
const message = error instanceof Error ? error.message : '';
|
|
19
|
+
const clean = message
|
|
20
|
+
.replace(/[a-z][a-z0-9+.-]*:\/\/\S+/gi, '<address>')
|
|
21
|
+
.replace(/\b[0-9a-f]{32,}\b/gi, '<value>')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim()
|
|
24
|
+
.slice(0, MAX_REASON);
|
|
25
|
+
return clean === '' ? name : `${name}: ${clean}`;
|
|
26
|
+
}
|
|
27
|
+
/** Seals and anchors each ledger in turn. Never throws for one ledger's failure. */
|
|
28
|
+
export async function sealLedgers(deps) {
|
|
29
|
+
const now = deps.now ?? (() => new Date());
|
|
30
|
+
const results = [];
|
|
31
|
+
for (const ledger of deps.ledgers) {
|
|
32
|
+
const result = await sealOne(deps, ledger, now);
|
|
33
|
+
results.push(result);
|
|
34
|
+
try {
|
|
35
|
+
deps.onResult?.(result);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// a reporting callback must not stop the tick
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
async function sealOne(deps, ledger, now) {
|
|
44
|
+
const base = { ledger: ledger.ledger, orgId: ledger.orgId, entryKey: ledger.entryKey };
|
|
45
|
+
let connection;
|
|
46
|
+
try {
|
|
47
|
+
connection = await deps.connector.open(ledger);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
outcome: 'failed',
|
|
53
|
+
anchored: 0,
|
|
54
|
+
reason: `could not open the ledger: ${plainFailure(error)}`,
|
|
55
|
+
at: now().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (isUnavailable(connection)) {
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
outcome: 'skipped',
|
|
62
|
+
anchored: 0,
|
|
63
|
+
reason: `not sealed, the ledger is not reachable: ${connection.unavailable}`,
|
|
64
|
+
at: now().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const notes = [];
|
|
68
|
+
let outcome = 'unchanged';
|
|
69
|
+
let treeSize;
|
|
70
|
+
let anchored = 0;
|
|
71
|
+
try {
|
|
72
|
+
try {
|
|
73
|
+
const checkpoint = await sealCheckpoint(connection.handle, {
|
|
74
|
+
ledger: ledger.ledger,
|
|
75
|
+
signer: deps.signer,
|
|
76
|
+
});
|
|
77
|
+
if (checkpoint) {
|
|
78
|
+
outcome = 'sealed';
|
|
79
|
+
treeSize = checkpoint.tree_size;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
notes.push('nothing sealed, no entry since the last checkpoint');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
...base,
|
|
88
|
+
outcome: 'failed',
|
|
89
|
+
anchored: 0,
|
|
90
|
+
reason: `sealing failed: ${plainFailure(error)}`,
|
|
91
|
+
at: now().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (deps.authority === null) {
|
|
95
|
+
notes.push('not anchored, no timestamp authority is configured');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
try {
|
|
99
|
+
anchored = await anchorCheckpoints(connection.handle, {
|
|
100
|
+
tsaUrl: deps.authority.tsaUrl,
|
|
101
|
+
provider: deps.authority.provider,
|
|
102
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
103
|
+
});
|
|
104
|
+
if (anchored === 0)
|
|
105
|
+
notes.push('nothing anchored, every checkpoint already has a time stamp');
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// The checkpoint is sealed and kept; the next tick asks the authority again.
|
|
109
|
+
notes.push(`anchoring failed, the next tick tries again: ${plainFailure(error)}`);
|
|
110
|
+
if (outcome === 'unchanged')
|
|
111
|
+
outcome = 'failed';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
try {
|
|
117
|
+
await connection.close();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// the connection is gone either way
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...base,
|
|
125
|
+
outcome,
|
|
126
|
+
...(treeSize === undefined ? {} : { treeSize }),
|
|
127
|
+
anchored,
|
|
128
|
+
...(notes.length > 0 ? { reason: notes.join('; ') } : {}),
|
|
129
|
+
at: now().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sealing role (foundry#929, audit#56): one database role per app ledger, apart from the app's runtime role.
|
|
3
|
+
* The runtime role appends events and never writes a checkpoint, a signing key or an anchor; this role writes
|
|
4
|
+
* those three and never touches the app's own tables. It exists so that the platform can seal a ledger without
|
|
5
|
+
* holding the app's runtime credential, and so that the app, which holds only the runtime credential, cannot.
|
|
6
|
+
*
|
|
7
|
+
* The password is made by the Worker app database apply, held at {@link auditSealerRef} and never recorded,
|
|
8
|
+
* logged or returned. Only the role name is recorded ({@link auditSealerScopeKey}); it is not a secret.
|
|
9
|
+
*/
|
|
10
|
+
/** The tables the sealing role may read and insert into. It has no UPDATE, DELETE or TRUNCATE on any of them. */
|
|
11
|
+
export declare const AUDIT_SEALER_INSERT_TABLES: readonly string[];
|
|
12
|
+
/** `audit_sealer.<entry>`: the sealing role's name, recorded after the role was made and its password held. */
|
|
13
|
+
export declare const auditSealerScopeKey: (entryKey: string) => string;
|
|
14
|
+
/** `audit_sealer_undelivered.<entry>`: the role name recorded before its password is held (no `:` in the key). */
|
|
15
|
+
export declare const auditSealerUndeliveredScopeKey: (entryKey: string) => string;
|
|
16
|
+
/** The held-key ref of one ledger's sealing password. */
|
|
17
|
+
export declare const auditSealerRef: (orgId: string, entryKey: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* The sealing role's name for an app database: `<database>_seal`, beside the runtime role's `<database>_rt`. Cut
|
|
20
|
+
* to Postgres's 63 bytes with a hash of the full name when the database name is long.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sealerRoleName(database: string): string;
|
|
23
|
+
/** The SQL the host runs as the database owner, after the audit migrations. Idempotent. */
|
|
24
|
+
export interface SealerRoleSql {
|
|
25
|
+
statements: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Makes `user` true: it can log in with `password`, is no superuser and bypasses no row security, may read and
|
|
29
|
+
* insert into the checkpoint, signing-key and anchor tables and run the two sealing functions, and holds nothing
|
|
30
|
+
* else. The grants are listed here and in `AUDIT_SEALER_GRANTS`; `audit/sealer-role.test.ts` runs a real
|
|
31
|
+
* checkpoint as this role to prove the list is enough.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sealerRoleSql(user: string, password: string): SealerRoleSql;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { AUDIT_SEALER_GRANTS } from '../modules/audit-ledger.js';
|
|
3
|
+
/**
|
|
4
|
+
* The sealing role (foundry#929, audit#56): one database role per app ledger, apart from the app's runtime role.
|
|
5
|
+
* The runtime role appends events and never writes a checkpoint, a signing key or an anchor; this role writes
|
|
6
|
+
* those three and never touches the app's own tables. It exists so that the platform can seal a ledger without
|
|
7
|
+
* holding the app's runtime credential, and so that the app, which holds only the runtime credential, cannot.
|
|
8
|
+
*
|
|
9
|
+
* The password is made by the Worker app database apply, held at {@link auditSealerRef} and never recorded,
|
|
10
|
+
* logged or returned. Only the role name is recorded ({@link auditSealerScopeKey}); it is not a secret.
|
|
11
|
+
*/
|
|
12
|
+
/** The tables the sealing role may read and insert into. It has no UPDATE, DELETE or TRUNCATE on any of them. */
|
|
13
|
+
export const AUDIT_SEALER_INSERT_TABLES = [
|
|
14
|
+
'audit_checkpoints',
|
|
15
|
+
'audit_signing_keys',
|
|
16
|
+
'audit_anchors',
|
|
17
|
+
];
|
|
18
|
+
/** `audit_sealer.<entry>`: the sealing role's name, recorded after the role was made and its password held. */
|
|
19
|
+
export const auditSealerScopeKey = (entryKey) => `audit_sealer.${entryKey}`;
|
|
20
|
+
/** `audit_sealer_undelivered.<entry>`: the role name recorded before its password is held (no `:` in the key). */
|
|
21
|
+
export const auditSealerUndeliveredScopeKey = (entryKey) => `audit_sealer_undelivered.${entryKey}`;
|
|
22
|
+
/** The held-key ref of one ledger's sealing password. */
|
|
23
|
+
export const auditSealerRef = (orgId, entryKey) => `foundry:audit-sealer:${orgId}:${entryKey}`;
|
|
24
|
+
/**
|
|
25
|
+
* The sealing role's name for an app database: `<database>_seal`, beside the runtime role's `<database>_rt`. Cut
|
|
26
|
+
* to Postgres's 63 bytes with a hash of the full name when the database name is long.
|
|
27
|
+
*/
|
|
28
|
+
export function sealerRoleName(database) {
|
|
29
|
+
const name = `${database}_seal`;
|
|
30
|
+
if (name.length <= 63)
|
|
31
|
+
return name;
|
|
32
|
+
const hash = createHash('sha256').update(name).digest('hex').slice(0, 10);
|
|
33
|
+
return `${name.slice(0, 63 - 11)}_${hash}`;
|
|
34
|
+
}
|
|
35
|
+
const ident = (name) => `"${name.replace(/"/g, '""')}"`;
|
|
36
|
+
const literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
37
|
+
/**
|
|
38
|
+
* Makes `user` true: it can log in with `password`, is no superuser and bypasses no row security, may read and
|
|
39
|
+
* insert into the checkpoint, signing-key and anchor tables and run the two sealing functions, and holds nothing
|
|
40
|
+
* else. The grants are listed here and in `AUDIT_SEALER_GRANTS`; `audit/sealer-role.test.ts` runs a real
|
|
41
|
+
* checkpoint as this role to prove the list is enough.
|
|
42
|
+
*/
|
|
43
|
+
export function sealerRoleSql(user, password) {
|
|
44
|
+
const role = ident(user);
|
|
45
|
+
const tables = AUDIT_SEALER_INSERT_TABLES.map(ident).join(', ');
|
|
46
|
+
return {
|
|
47
|
+
statements: [
|
|
48
|
+
`do $$ begin if not exists (select from pg_roles where rolname = ${literal(user)}) then create role ${role} login; end if; end $$`,
|
|
49
|
+
`alter role ${role} with login password ${literal(password)} nosuperuser nocreatedb nocreaterole noinherit nobypassrls`,
|
|
50
|
+
`grant usage on schema public to ${role}`,
|
|
51
|
+
`grant select, insert on ${tables} to ${role}`,
|
|
52
|
+
`revoke update, delete, truncate on ${tables} from ${role}`,
|
|
53
|
+
...AUDIT_SEALER_GRANTS.map((fn) => `grant execute on function ${fn} to ${role}`),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CheckpointSigner } from '@wtfalch/audit-sealing';
|
|
2
|
+
/**
|
|
3
|
+
* The platform's checkpoint signer (foundry#929, audit#56). One Ed25519 key for every app ledger the platform
|
|
4
|
+
* seals, held by foundry and never by the company's app or its developers: the app's database stores the
|
|
5
|
+
* checkpoints and the public key, nothing that signs.
|
|
6
|
+
*
|
|
7
|
+
* Custody: foundry has no signing use of the `keys` service (it only delivers a custody credential to a Worker
|
|
8
|
+
* app), so the key follows the way foundry already holds a platform secret it made (`calls-sfu-keys.ts`): made
|
|
9
|
+
* once, delivered to the host's held keys, read back on every later run. The 32-byte seed is the only secret.
|
|
10
|
+
* It lives in {@link AuditSignerKeysPort}, in the signer's closure and nowhere else: not a plan, a log, an
|
|
11
|
+
* error, a recorded id or a result.
|
|
12
|
+
*/
|
|
13
|
+
/** The held-key ref of the platform signer's seed. */
|
|
14
|
+
export declare const AUDIT_SIGNER_REF = "foundry:audit-signer:seed";
|
|
15
|
+
/**
|
|
16
|
+
* Where the seed is held. `deliverSeed` must be create-if-absent at the store: when two applies both see `null`
|
|
17
|
+
* and both mint, the first delivery wins and the second `resolveSeed` returns the winner's seed.
|
|
18
|
+
*/
|
|
19
|
+
export interface AuditSignerKeysPort {
|
|
20
|
+
resolveSeed(): Promise<string | null>;
|
|
21
|
+
deliverSeed(seedHex: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/** A fresh seed from the CSPRNG, 64 lower-case hex characters. */
|
|
24
|
+
export declare function mintSignerSeed(): string;
|
|
25
|
+
/** The signer for a seed. The seed is not readable back from it. */
|
|
26
|
+
export declare function signerFromSeed(seedHex: string): CheckpointSigner;
|
|
27
|
+
/**
|
|
28
|
+
* The platform signer: the held seed, or a new one made and held first. Whether it was made is returned so the
|
|
29
|
+
* caller can record why a new public key appeared. Refuses a held value that is not a seed, without echoing it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ensurePlatformSigner(keys: AuditSignerKeysPort, mint?: () => string,
|
|
32
|
+
/**
|
|
33
|
+
* The public key already recorded as the platform signer, if any. When one is recorded and the held seed
|
|
34
|
+
* reads missing or empty, no new seed is ever made: a new key would silently replace the one earlier
|
|
35
|
+
* checkpoints were sealed under. The run stops and nothing is written.
|
|
36
|
+
*/
|
|
37
|
+
recordedPublicKey?: string): Promise<{
|
|
38
|
+
signer: CheckpointSigner;
|
|
39
|
+
created: boolean;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, sign as edSign, generateKeyPairSync, } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* The platform's checkpoint signer (foundry#929, audit#56). One Ed25519 key for every app ledger the platform
|
|
4
|
+
* seals, held by foundry and never by the company's app or its developers: the app's database stores the
|
|
5
|
+
* checkpoints and the public key, nothing that signs.
|
|
6
|
+
*
|
|
7
|
+
* Custody: foundry has no signing use of the `keys` service (it only delivers a custody credential to a Worker
|
|
8
|
+
* app), so the key follows the way foundry already holds a platform secret it made (`calls-sfu-keys.ts`): made
|
|
9
|
+
* once, delivered to the host's held keys, read back on every later run. The 32-byte seed is the only secret.
|
|
10
|
+
* It lives in {@link AuditSignerKeysPort}, in the signer's closure and nowhere else: not a plan, a log, an
|
|
11
|
+
* error, a recorded id or a result.
|
|
12
|
+
*/
|
|
13
|
+
/** The held-key ref of the platform signer's seed. */
|
|
14
|
+
export const AUDIT_SIGNER_REF = 'foundry:audit-signer:seed';
|
|
15
|
+
const SEED_HEX = /^[0-9a-f]{64}$/;
|
|
16
|
+
// PKCS#8 wrapper for a raw Ed25519 seed (RFC 8410).
|
|
17
|
+
const PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
18
|
+
function privateKeyOf(seedHex) {
|
|
19
|
+
return createPrivateKey({
|
|
20
|
+
key: Buffer.concat([PKCS8_PREFIX, Buffer.from(seedHex, 'hex')]),
|
|
21
|
+
format: 'der',
|
|
22
|
+
type: 'pkcs8',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function publicKeyHexOf(key) {
|
|
26
|
+
const x = createPublicKey(key).export({ format: 'jwk' }).x;
|
|
27
|
+
if (typeof x !== 'string')
|
|
28
|
+
throw new Error('the audit signer key has no public half');
|
|
29
|
+
return Buffer.from(x, 'base64url').toString('hex');
|
|
30
|
+
}
|
|
31
|
+
/** A fresh seed from the CSPRNG, 64 lower-case hex characters. */
|
|
32
|
+
export function mintSignerSeed() {
|
|
33
|
+
const { privateKey } = generateKeyPairSync('ed25519');
|
|
34
|
+
const der = privateKey.export({ format: 'der', type: 'pkcs8' });
|
|
35
|
+
return der.subarray(PKCS8_PREFIX.length).toString('hex');
|
|
36
|
+
}
|
|
37
|
+
/** The signer for a seed. The seed is not readable back from it. */
|
|
38
|
+
export function signerFromSeed(seedHex) {
|
|
39
|
+
if (!SEED_HEX.test(seedHex))
|
|
40
|
+
throw new Error('the audit signer seed is not 32 bytes of hex');
|
|
41
|
+
const key = privateKeyOf(seedHex);
|
|
42
|
+
const publicKey = publicKeyHexOf(key);
|
|
43
|
+
return {
|
|
44
|
+
publicKey,
|
|
45
|
+
async sign(message) {
|
|
46
|
+
return new Uint8Array(edSign(null, message, key));
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The platform signer: the held seed, or a new one made and held first. Whether it was made is returned so the
|
|
52
|
+
* caller can record why a new public key appeared. Refuses a held value that is not a seed, without echoing it.
|
|
53
|
+
*/
|
|
54
|
+
export async function ensurePlatformSigner(keys, mint = mintSignerSeed,
|
|
55
|
+
/**
|
|
56
|
+
* The public key already recorded as the platform signer, if any. When one is recorded and the held seed
|
|
57
|
+
* reads missing or empty, no new seed is ever made: a new key would silently replace the one earlier
|
|
58
|
+
* checkpoints were sealed under. The run stops and nothing is written.
|
|
59
|
+
*/
|
|
60
|
+
recordedPublicKey) {
|
|
61
|
+
let seed = await keys.resolveSeed();
|
|
62
|
+
let created = false;
|
|
63
|
+
if (seed === null || seed === '') {
|
|
64
|
+
if (recordedPublicKey) {
|
|
65
|
+
throw new Error(`the platform audit signing key is missing at ${AUDIT_SIGNER_REF}; restore it, nothing was changed`);
|
|
66
|
+
}
|
|
67
|
+
await keys.deliverSeed(mint());
|
|
68
|
+
seed = await keys.resolveSeed();
|
|
69
|
+
created = true;
|
|
70
|
+
if (seed === null || seed === '') {
|
|
71
|
+
throw new Error(`the audit signer seed was delivered but ${AUDIT_SIGNER_REF} reads back empty`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!SEED_HEX.test(seed)) {
|
|
75
|
+
throw new Error(`the held value at ${AUDIT_SIGNER_REF} is not an audit signer seed`);
|
|
76
|
+
}
|
|
77
|
+
return { signer: signerFromSeed(seed), created };
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { LedgerConnector } from './connector.js';
|
|
3
|
+
import { type AuditPolicy } from './registry.js';
|
|
4
|
+
import { type SealResult, type TimestampAuthority } from './seal.js';
|
|
5
|
+
/**
|
|
6
|
+
* The scheduled sealing run (foundry#929). Foundry has no scheduler: the host calls
|
|
7
|
+
* `FoundryPlanApply.runAuditSealTick()` every {@link AUDIT_SEAL_EVERY_MINUTES} minutes, the way it calls
|
|
8
|
+
* `runReleaseTick()` every 10, and that runs this for every declared org. It seals the ledgers the `audit:ledger`
|
|
9
|
+
* step registered, nothing else, and writes one log line per ledger that says what happened and, when nothing did,
|
|
10
|
+
* why (a ledger with no new entries, an unreachable database, no timestamp authority).
|
|
11
|
+
*/
|
|
12
|
+
/** What the host gives `FoundryPlanApply` to run the tick. */
|
|
13
|
+
export interface AuditSealingOptions {
|
|
14
|
+
/** Where the platform signer's seed is held (`foundry:audit-signer:seed`). */
|
|
15
|
+
heldKeys: {
|
|
16
|
+
resolve(ref: string): Promise<string | null | undefined>;
|
|
17
|
+
};
|
|
18
|
+
/** How a ledger is reached as its sealing role: `createLedgerConnector`. */
|
|
19
|
+
connector: LedgerConnector;
|
|
20
|
+
/** The plan's retention period per org and the legal holds (the host's, from billing). */
|
|
21
|
+
policy: AuditPolicy;
|
|
22
|
+
/** The RFC 3161 authority, or `null` to seal without anchoring (each result and the log say so). */
|
|
23
|
+
authority: TimestampAuthority | null;
|
|
24
|
+
fetch?: typeof fetch;
|
|
25
|
+
/** One plain line per ledger and per skipped step. Defaults to `console.info`. */
|
|
26
|
+
log?: (line: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface AuditSealTickDeps extends AuditSealingOptions {
|
|
29
|
+
orgs: readonly DeclaredOrg[];
|
|
30
|
+
recorded(org: DeclaredOrg): Promise<Readonly<Record<string, string>>>;
|
|
31
|
+
/** Whether the actor may seal this org's ledgers (the apply permission). */
|
|
32
|
+
allowed(org: DeclaredOrg): Promise<boolean>;
|
|
33
|
+
now?: () => Date;
|
|
34
|
+
}
|
|
35
|
+
export declare const NO_AUTHORITY_LINE = "audit sealing: no timestamp authority is configured, so checkpoints are sealed and not anchored";
|
|
36
|
+
export declare function runAuditSealTick(deps: AuditSealTickDeps): Promise<SealResult[]>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AUDIT_SIGNER_PUBLIC_KEY_SCOPE_KEY } from '../plan/entry-audit.js';
|
|
2
|
+
import { registeredAppLedgers } from './registry.js';
|
|
3
|
+
import { sealLedgers, } from './seal.js';
|
|
4
|
+
import { AUDIT_SIGNER_REF, signerFromSeed } from './signer.js';
|
|
5
|
+
export const NO_AUTHORITY_LINE = 'audit sealing: no timestamp authority is configured, so checkpoints are sealed and not anchored';
|
|
6
|
+
const describe = (r) => `audit seal ${r.ledger}: ${r.outcome}${r.reason ? `, ${r.reason}` : ''}`;
|
|
7
|
+
export async function runAuditSealTick(deps) {
|
|
8
|
+
const log = deps.log ?? ((line) => console.info(line));
|
|
9
|
+
const now = deps.now ?? (() => new Date());
|
|
10
|
+
const results = [];
|
|
11
|
+
const skip = (ledger, reason) => {
|
|
12
|
+
const result = {
|
|
13
|
+
ledger: ledger.ledger,
|
|
14
|
+
orgId: ledger.orgId,
|
|
15
|
+
entryKey: ledger.entryKey,
|
|
16
|
+
outcome: 'skipped',
|
|
17
|
+
anchored: 0,
|
|
18
|
+
reason,
|
|
19
|
+
at: now().toISOString(),
|
|
20
|
+
};
|
|
21
|
+
results.push(result);
|
|
22
|
+
log(describe(result));
|
|
23
|
+
};
|
|
24
|
+
const due = [];
|
|
25
|
+
for (const org of deps.orgs) {
|
|
26
|
+
try {
|
|
27
|
+
const ledgers = registeredAppLedgers(org, await deps.recorded(org), deps.policy);
|
|
28
|
+
if (ledgers.length > 0)
|
|
29
|
+
due.push({ org, ledgers });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
log(`audit sealing: ${org.slug} skipped, its records could not be read; the other orgs go on`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (due.length === 0)
|
|
36
|
+
return results;
|
|
37
|
+
if (deps.authority === null)
|
|
38
|
+
log(NO_AUTHORITY_LINE);
|
|
39
|
+
let signer = null;
|
|
40
|
+
let noSigner = 'not sealed, the platform signer key is not held';
|
|
41
|
+
try {
|
|
42
|
+
const seed = await deps.heldKeys.resolve(AUDIT_SIGNER_REF);
|
|
43
|
+
if (seed)
|
|
44
|
+
signer = signerFromSeed(seed);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
noSigner = 'not sealed, the platform signer key could not be read';
|
|
48
|
+
}
|
|
49
|
+
for (const { org, ledgers } of due) {
|
|
50
|
+
if (!signer) {
|
|
51
|
+
for (const ledger of ledgers)
|
|
52
|
+
skip(ledger, noSigner);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// One org that throws (permission check, record read, a ledger) never stops the orgs after it.
|
|
56
|
+
try {
|
|
57
|
+
if (!(await deps.allowed(org))) {
|
|
58
|
+
for (const ledger of ledgers)
|
|
59
|
+
skip(ledger, 'not sealed, the actor may not apply this org');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const recordedKey = (await deps.recorded(org))[AUDIT_SIGNER_PUBLIC_KEY_SCOPE_KEY];
|
|
63
|
+
if (recordedKey && recordedKey !== signer.publicKey) {
|
|
64
|
+
for (const ledger of ledgers) {
|
|
65
|
+
skip(ledger, 'not sealed, the held signer key is not the one recorded as the platform signer');
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const sealed = await sealLedgers({
|
|
70
|
+
ledgers,
|
|
71
|
+
connector: deps.connector,
|
|
72
|
+
signer,
|
|
73
|
+
authority: deps.authority,
|
|
74
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
75
|
+
now,
|
|
76
|
+
onResult: (r) => log(describe(r)),
|
|
77
|
+
});
|
|
78
|
+
results.push(...sealed);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
for (const ledger of ledgers) {
|
|
82
|
+
skip(ledger, 'not sealed, a check or read for this org failed; the other orgs go on');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CATALOGUE_EN: Readonly<Record<string, string>>;
|