@fabrika/app 0.8.2 → 0.9.0-cloudflare.2
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/package.json +40 -24
- package/skill/SKILL.md +29 -3
- package/skill/references/app-setup.md +29 -11
- package/skill/references/dev.md +2 -1
- package/skill/references/endpoint.md +12 -3
- package/skill/references/errors.md +39 -35
- package/skill/references/files.md +4 -2
- package/skill/references/jobs.md +9 -0
- package/skill/references/table-and-migration.md +19 -2
- package/skill/references/tests.md +33 -1
- package/skill/references/webhooks-links-connectors.md +1 -1
- package/src/build-recipe.ts +198 -0
- package/src/build.ts +60 -4
- package/src/bun.ts +33 -8
- package/src/cloudflare/application.ts +352 -0
- package/src/cloudflare/bindings.ts +159 -0
- package/src/cloudflare/names.ts +16 -0
- package/src/cloudflare/ops.ts +246 -0
- package/src/cloudflare/r2-files.ts +172 -0
- package/src/cloudflare.ts +99 -146
- package/src/contract/app-id.ts +5 -0
- package/src/contract/artifact-schema.ts +8 -1
- package/src/contract/build.ts +68 -11
- package/src/contract/compile.ts +1 -0
- package/src/contract/context.ts +9 -0
- package/src/contract/declaration.ts +2 -0
- package/src/contract/define.ts +28 -2
- package/src/contract/group.ts +10 -1
- package/src/contract/kit.ts +6 -2
- package/src/contract/platform.ts +3 -2
- package/src/contract/vocabulary.ts +26 -8
- package/src/dev/changes.ts +35 -0
- package/src/dev/contract-digest.ts +17 -0
- package/src/dev/index.ts +11 -0
- package/src/dev/launcher.ts +713 -0
- package/src/dev/output.ts +72 -0
- package/src/dev/wire.ts +136 -0
- package/src/disk-assets.ts +8 -3
- package/src/files/archive.ts +2 -2
- package/src/files/job-names.ts +12 -0
- package/src/files/kinds.ts +19 -7
- package/src/files/scan.ts +3 -4
- package/src/files/streams.ts +3 -1
- package/src/implement/anonymous.ts +11 -1
- package/src/implement/boot.ts +70 -7
- package/src/implement/bounded.ts +21 -2
- package/src/implement/capability.ts +18 -3
- package/src/implement/dispatch.ts +251 -147
- package/src/implement/egress.ts +341 -8
- package/src/implement/failures.ts +17 -1
- package/src/implement/faults.ts +1 -0
- package/src/implement/index.ts +1 -0
- package/src/implement/invocation.ts +18 -9
- package/src/implement/principal.ts +25 -2
- package/src/implement/terminals.ts +93 -0
- package/src/implement/workload.ts +24 -13
- package/src/jobs/report.ts +5 -1
- package/src/jobs/runner.ts +40 -19
- package/src/jobs/schedule.ts +7 -1
- package/src/jobs/scrub.ts +14 -1
- package/src/spa-fallback.ts +12 -0
- package/src/testing/endpoint-access.ts +307 -152
- package/src/testing/isolation.ts +8 -1
- package/src/testing/kit.ts +20 -2
- package/src/testing/proxy.ts +5 -5
- package/types/src/agent-skill.d.ts +29 -0
- package/types/src/build-recipe.d.ts +70 -0
- package/types/src/build.d.ts +29 -0
- package/types/src/bun.d.ts +66 -0
- package/types/src/client/caller.d.ts +19 -0
- package/types/src/client/contract.d.ts +131 -0
- package/types/src/client/error-reporting.d.ts +66 -0
- package/types/src/client/index.d.ts +4 -0
- package/types/src/client/rpc.d.ts +14 -0
- package/types/src/client/share.d.ts +2 -0
- package/types/src/client/transport.d.ts +42 -0
- package/types/src/cloudflare/application.d.ts +42 -0
- package/types/src/cloudflare/bindings.d.ts +33 -0
- package/types/src/cloudflare/names.d.ts +11 -0
- package/types/src/cloudflare/ops.d.ts +46 -0
- package/types/src/cloudflare/r2-files.d.ts +9 -0
- package/types/src/cloudflare.d.ts +42 -0
- package/types/src/contract/app-id.d.ts +2 -0
- package/types/src/contract/artifact-schema.d.ts +8 -0
- package/types/src/contract/attachment.d.ts +41 -0
- package/types/src/contract/build.d.ts +12 -0
- package/types/src/contract/compile.d.ts +11 -0
- package/types/src/contract/connect.d.ts +51 -0
- package/types/src/contract/context.d.ts +510 -0
- package/types/src/contract/declaration.d.ts +307 -0
- package/types/src/contract/defaults.d.ts +208 -0
- package/types/src/contract/define.d.ts +376 -0
- package/types/src/contract/file.d.ts +44 -0
- package/types/src/contract/group.d.ts +282 -0
- package/types/src/contract/handled.d.ts +18 -0
- package/types/src/contract/index.d.ts +19 -0
- package/types/src/contract/kit.d.ts +211 -0
- package/types/src/contract/platform.d.ts +9 -0
- package/types/src/contract/problems.d.ts +14 -0
- package/types/src/contract/redirect.d.ts +25 -0
- package/types/src/contract/schema-from-shape.d.ts +10 -0
- package/types/src/contract/schema-shape.d.ts +5 -0
- package/types/src/contract/snapshot-source.d.ts +15 -0
- package/types/src/contract/token.d.ts +58 -0
- package/types/src/contract/vocabulary.d.ts +32 -0
- package/types/src/contract/webhook-binding.d.ts +24 -0
- package/types/src/contract/webhook-scheme.d.ts +23 -0
- package/types/src/dev/changes.d.ts +7 -0
- package/types/src/dev/contract-digest.d.ts +1 -0
- package/types/src/dev/index.d.ts +2 -0
- package/types/src/dev/launcher.d.ts +32 -0
- package/types/src/dev/output.d.ts +28 -0
- package/types/src/dev/wire.d.ts +126 -0
- package/types/src/disk-assets.d.ts +6 -0
- package/types/src/errors.d.ts +46 -0
- package/types/src/fetch.d.ts +7 -0
- package/types/src/files/archive.d.ts +26 -0
- package/types/src/files/clamscan.d.ts +32 -0
- package/types/src/files/handle.d.ts +106 -0
- package/types/src/files/job-names.d.ts +9 -0
- package/types/src/files/kinds.d.ts +101 -0
- package/types/src/files/scan.d.ts +78 -0
- package/types/src/files/sniff.d.ts +7 -0
- package/types/src/files/streams.d.ts +17 -0
- package/types/src/files/treatment.d.ts +18 -0
- package/types/src/from-row.d.ts +20 -0
- package/types/src/honeypot.d.ts +2 -0
- package/types/src/implement/anonymous.d.ts +28 -0
- package/types/src/implement/attachment.d.ts +14 -0
- package/types/src/implement/boot.d.ts +120 -0
- package/types/src/implement/bounded.d.ts +32 -0
- package/types/src/implement/browser-config.d.ts +19 -0
- package/types/src/implement/capability.d.ts +144 -0
- package/types/src/implement/challenge.d.ts +75 -0
- package/types/src/implement/client-address.d.ts +11 -0
- package/types/src/implement/connect.d.ts +67 -0
- package/types/src/implement/crashes.d.ts +31 -0
- package/types/src/implement/dispatch.d.ts +221 -0
- package/types/src/implement/egress.d.ts +49 -0
- package/types/src/implement/environment.d.ts +14 -0
- package/types/src/implement/failures.d.ts +39 -0
- package/types/src/implement/faults.d.ts +45 -0
- package/types/src/implement/headers.d.ts +7 -0
- package/types/src/implement/implement.d.ts +58 -0
- package/types/src/implement/index.d.ts +25 -0
- package/types/src/implement/invocation.d.ts +125 -0
- package/types/src/implement/membership.d.ts +201 -0
- package/types/src/implement/multipart.d.ts +36 -0
- package/types/src/implement/params.d.ts +7 -0
- package/types/src/implement/principal.d.ts +57 -0
- package/types/src/implement/project.d.ts +11 -0
- package/types/src/implement/public.d.ts +61 -0
- package/types/src/implement/serve.d.ts +25 -0
- package/types/src/implement/terminals.d.ts +33 -0
- package/types/src/implement/tokens.d.ts +49 -0
- package/types/src/implement/types.d.ts +243 -0
- package/types/src/implement/urls.d.ts +15 -0
- package/types/src/implement/verifiers.d.ts +19 -0
- package/types/src/implement/webhook.d.ts +55 -0
- package/types/src/implement/workload.d.ts +56 -0
- package/types/src/implement/xlsx.d.ts +6 -0
- package/types/src/index.d.ts +12 -0
- package/types/src/jobs/exception.d.ts +40 -0
- package/types/src/jobs/mail.d.ts +67 -0
- package/types/src/jobs/report.d.ts +56 -0
- package/types/src/jobs/runner.d.ts +154 -0
- package/types/src/jobs/schedule.d.ts +17 -0
- package/types/src/jobs/scrub.d.ts +12 -0
- package/types/src/jobs/types.d.ts +98 -0
- package/types/src/react/caller.d.ts +30 -0
- package/types/src/react/errors.d.ts +40 -0
- package/types/src/react/hooks.d.ts +110 -0
- package/types/src/react/index.d.ts +5 -0
- package/types/src/react/provider.d.ts +58 -0
- package/types/src/react/store.d.ts +68 -0
- package/types/src/react/testing.d.ts +28 -0
- package/types/src/react/types.d.ts +33 -0
- package/types/src/rpc/builder.d.ts +52 -0
- package/types/src/rpc/contract.d.ts +23 -0
- package/types/src/rpc/dispatcher.d.ts +11 -0
- package/types/src/rpc/types.d.ts +62 -0
- package/types/src/service/define.d.ts +42 -0
- package/types/src/service/index.d.ts +3 -0
- package/types/src/service/serve.d.ts +28 -0
- package/types/src/spa-fallback.d.ts +2 -0
- package/types/src/standard-schema.d.ts +55 -0
- package/types/src/testing/caller.d.ts +95 -0
- package/types/src/testing/challenge.d.ts +11 -0
- package/types/src/testing/dev.d.ts +97 -0
- package/types/src/testing/endpoint-access.d.ts +131 -0
- package/types/src/testing/index.d.ts +12 -0
- package/types/src/testing/isolation.d.ts +3 -0
- package/types/src/testing/kit.d.ts +187 -0
- package/types/src/testing/membership.d.ts +132 -0
- package/types/src/testing/policy-isolation.d.ts +113 -0
- package/types/src/testing/preload.d.ts +1 -0
- package/types/src/testing/proxy.d.ts +40 -0
- package/types/src/testing/runner.d.ts +8 -0
- package/types/src/testing/sample-file.d.ts +5 -0
- package/types/src/testing/status.d.ts +4 -0
- package/types/src/undumpable.d.ts +5 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { type ContractArtifact, type Scope } from '@fabrika/auth-core';
|
|
2
|
+
import type { Tables } from '@fabrika/db';
|
|
3
|
+
import { type SampleValue, type TestDatabase } from '@fabrika/db/testing';
|
|
4
|
+
import type { AnyContract } from '../contract/define';
|
|
5
|
+
import type { ContractImplementation } from '../implement/implement';
|
|
6
|
+
export interface DeniedOutcome {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `answered` when every answer to the own scope is one the application gave: a 2xx, or a 4xx other than 401, 403 and
|
|
11
|
+
* 404 (a precondition's 409 included). Otherwise `status` is the first other answer. `leaked` are the values of the
|
|
12
|
+
* other scope's rows that the answers held: keys, parents, scope values and text markers.
|
|
13
|
+
*/
|
|
14
|
+
export type LeakOutcome = {
|
|
15
|
+
readonly answered: true;
|
|
16
|
+
readonly leaked: readonly string[];
|
|
17
|
+
} | {
|
|
18
|
+
readonly answered: false;
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly leaked: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
/** `answered` unless the caller got a 401, a 403 or a 5xx; otherwise that status. */
|
|
23
|
+
export type ReachOutcome = {
|
|
24
|
+
readonly answered: true;
|
|
25
|
+
} | {
|
|
26
|
+
readonly answered: false;
|
|
27
|
+
readonly status: number;
|
|
28
|
+
};
|
|
29
|
+
/** Column → value of the row of the table that owns the endpoint's dimension, in both scopes: the author's seed. */
|
|
30
|
+
export type Seed = Readonly<Record<string, SampleValue>>;
|
|
31
|
+
/**
|
|
32
|
+
* Table → column → value: one row of each application table the handler reads for the caller's own scope, seeded
|
|
33
|
+
* before a leak or an own case asks and removed once it is over, with every row that refers to it, so no other case
|
|
34
|
+
* finds it. Both scopes may see it, so its values are never counted as the other scope's.
|
|
35
|
+
*/
|
|
36
|
+
export type AppRows = Readonly<Record<string, Seed>>;
|
|
37
|
+
export interface EndpointAccessKit {
|
|
38
|
+
/**
|
|
39
|
+
* A caller holding the endpoint's action in one scope names an object (`on`) or a scope value (`in`) of another;
|
|
40
|
+
* `input` is the rest of a valid input, and its `from` field is replaced.
|
|
41
|
+
*/
|
|
42
|
+
denied(endpoint: string, input: Readonly<Record<string, unknown>>, seed?: Seed): Promise<DeniedOutcome>;
|
|
43
|
+
/**
|
|
44
|
+
* A caller of the other scope asks first, then a caller of one scope asks for its own object (`on`), its list
|
|
45
|
+
* (`over`) or, for every table that lies in the dimension, its scope value (`in`). `appRows` are seeded first, as
|
|
46
|
+
* for `own`.
|
|
47
|
+
*/
|
|
48
|
+
leaked(endpoint: string, input: Readonly<Record<string, unknown>>, seed?: Seed, appRows?: AppRows): Promise<LeakOutcome>;
|
|
49
|
+
/**
|
|
50
|
+
* A caller that holds only the endpoint's action asks for its own object: a person or a script in its scope, or, on
|
|
51
|
+
* a link mount, the holder of a link bound to the object. A write action reads its own target. Where IAM requires more
|
|
52
|
+
* of the caller, it holds that too: a member manager an assigning role and the assigned role's actions, a link's
|
|
53
|
+
* issuer the actions the link grants. `appRows` are the rows of application tables its handler reads (the guide a link
|
|
54
|
+
* holder downloads), each seeded first.
|
|
55
|
+
*/
|
|
56
|
+
own(endpoint: string, input: Readonly<Record<string, unknown>>, seed?: Seed, appRows?: AppRows): Promise<DeniedOutcome>;
|
|
57
|
+
/**
|
|
58
|
+
* An `app` or a `creates` endpoint asked by a caller who holds its action in one value of a dimension only, never
|
|
59
|
+
* globally: refused (403), by the proxy for `app` and by IAM for `creates`, since both need a global grant.
|
|
60
|
+
*/
|
|
61
|
+
scoped(endpoint: string, input: Readonly<Record<string, unknown>>): Promise<DeniedOutcome>;
|
|
62
|
+
/**
|
|
63
|
+
* An `app` or a `creates` endpoint asked by a caller who holds its action as a global grant, and nothing else:
|
|
64
|
+
* `answered` unless the answer is a 401, a 403 or a 5xx.
|
|
65
|
+
*/
|
|
66
|
+
global(endpoint: string, input: Readonly<Record<string, unknown>>): Promise<ReachOutcome>;
|
|
67
|
+
/**
|
|
68
|
+
* One capability of an endpoint of several, refused while every other one passes: the caller holds each other one's
|
|
69
|
+
* action in a scope of its own and addresses its object or value there (or, for `over`, holds the action globally).
|
|
70
|
+
* For an `on` or `in` capability it also holds the named one's action in its own scope and addresses the other's
|
|
71
|
+
* object (404) or value (403); for `over` it holds the named action nowhere (403). `seed` is the named one's.
|
|
72
|
+
*/
|
|
73
|
+
capability(endpoint: string, capability: string, input: Readonly<Record<string, unknown>>, seed?: Seed): Promise<DeniedOutcome>;
|
|
74
|
+
/**
|
|
75
|
+
* A public procedure asked by anyone, with no credential, from a page of its own origin: `answered` unless the answer
|
|
76
|
+
* is a 401, a 403, a 5xx or a failed challenge. A procedure with a challenge is a 400 `challenge_failed`: the kit's
|
|
77
|
+
* challenge never passes.
|
|
78
|
+
*/
|
|
79
|
+
anyone(endpoint: string, input: Readonly<Record<string, unknown>>): Promise<ReachOutcome>;
|
|
80
|
+
}
|
|
81
|
+
export interface EndpointAccessOptions {
|
|
82
|
+
/**
|
|
83
|
+
* The file every case of an endpoint sends in a file field instead of the kit's sample, by endpoint id and input
|
|
84
|
+
* field: a file of the application's repository that a handler inspecting the content accepts (a valid package). Its
|
|
85
|
+
* type is the first of the field's kind that its bytes are; a file of none of them throws. `files` of the fixtures.
|
|
86
|
+
*/
|
|
87
|
+
readonly files?: Readonly<Record<string, Readonly<Record<string, URL>>>>;
|
|
88
|
+
/**
|
|
89
|
+
* The error types of the refusals (400) an endpoint's handler answers its own object with when the case's input meets
|
|
90
|
+
* no domain rule the kit can satisfy, by endpoint id: such a 400 is the handler's own answer, so the request passed
|
|
91
|
+
* the access form, and the endpoint's own case gets it as `{ status: 400 }` instead of failing; so does the other
|
|
92
|
+
* scope's request a leak case makes first. The own scope's answer of a leak case, and a global or capability case,
|
|
93
|
+
* still throw on it (an answer that holds nothing proves no isolation), so does any other 400, and an input the schema
|
|
94
|
+
* refuses (`validation`) always does. `refusals` of the fixtures.
|
|
95
|
+
*/
|
|
96
|
+
readonly refusals?: Readonly<Record<string, readonly string[]>>;
|
|
97
|
+
/**
|
|
98
|
+
* Known values for text columns of the row the endpoint addresses (the row of the table that owns its dimension), by
|
|
99
|
+
* endpoint id: the value in the own scope's row, and `<value>-other` in the other scope's, which counts among the
|
|
100
|
+
* other scope's values a leak shows. For a handler whose rule ties a column to the input or a file fixture (a package
|
|
101
|
+
* that names the skill's slug). `pins` of the fixtures.
|
|
102
|
+
*/
|
|
103
|
+
readonly pins?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
104
|
+
}
|
|
105
|
+
export type Grant = {
|
|
106
|
+
readonly action: string;
|
|
107
|
+
readonly scope: {
|
|
108
|
+
readonly type: string;
|
|
109
|
+
readonly value: string;
|
|
110
|
+
};
|
|
111
|
+
} | {
|
|
112
|
+
readonly role: string;
|
|
113
|
+
readonly scope: {
|
|
114
|
+
readonly type: string;
|
|
115
|
+
readonly value: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The least a member manager holds in `scope` for IAM to let it give or remove the roles a case may ask for: the
|
|
120
|
+
* endpoint's action, one assigning role of each (`assignableBy`) that adds the fewest actions beyond those, and the
|
|
121
|
+
* actions of each as plain grants. The roles a case asks for are the membership roles its input names; an input that
|
|
122
|
+
* names none may ask for any, so the caller then covers every membership role of the dimension. Where one role assigns
|
|
123
|
+
* them all, it is the only role held. Every role is held in `scope`, an application-wide assigner too: IAM counts any
|
|
124
|
+
* assigning role held there, and a grant in `scope` reaches no other value.
|
|
125
|
+
*/
|
|
126
|
+
export declare const managerGrants: (artifact: Pick<ContractArtifact, "roles">, action: string, input: Readonly<Record<string, unknown>>, scope: Scope) => Grant[];
|
|
127
|
+
/**
|
|
128
|
+
* The endpoint access kit of `app`, whose data layer is `scoped(tables)`, over `postgres`. It boots `app` once, so the
|
|
129
|
+
* application's layer is bound here and nowhere else in the process.
|
|
130
|
+
*/
|
|
131
|
+
export declare function endpointAccessKit<DB, P>(app: ContractImplementation<AnyContract>, tables: Tables<DB, P>, postgres: TestDatabase, options?: EndpointAccessOptions): EndpointAccessKit;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { IamMembershipRpc } from '@fabrika/auth-core';
|
|
2
|
+
export { type MemoryEmail, MemoryEmailSender, type MemoryHandoff } from '@fabrika/email/memory';
|
|
3
|
+
export type { FileScanner } from '../files/scan';
|
|
4
|
+
export type { ChallengeFetch } from '../implement/challenge';
|
|
5
|
+
export { mailKeyPrefix, type MailKeyScope, transportMailKey } from '../jobs/mail';
|
|
6
|
+
export type { HandlerErrorSink } from '../jobs/runner';
|
|
7
|
+
export type { LabelledTestCaller, LinkTestCaller, PersonTestCaller, TestCaller, TestCallerKind, TestCallerOptions, TestGrant, TestLink, TestScope, } from './caller';
|
|
8
|
+
export { passingChallenge } from './challenge';
|
|
9
|
+
export { DEV_OUTBOX_ORIGIN, type DevKit, devKit, type DevLink, type DevMail, type DevOutboxMessage, type DevOutside, type DevPersona, type DevScopeValue, type DevSeed, type DevSeedResult, } from './dev';
|
|
10
|
+
export { type JobName, TEST_ENVIRONMENT, TEST_ISSUER, type TestConnection, type TestKit, testKit, type TestKitOptions } from './kit';
|
|
11
|
+
export type { SentInvitation, TestMembership } from './membership';
|
|
12
|
+
export { statusOf } from './status';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { type DeniedOutcome, type EndpointAccessKit, endpointAccessKit, type EndpointAccessOptions, type LeakOutcome, type ReachOutcome, } from './endpoint-access';
|
|
2
|
+
export { type AppCaller, type AppCountOutcome, type AppIsolationKit, appPolicyIsolationKit, type ChildIsolationRow, type CountOutcome, type IsolationOutcome, type IsolationRow, type ListOutcome, type MisuseOutcome, type PolicyIsolationKit, policyIsolationKit, type ReadOutcome, } from './policy-isolation';
|
|
3
|
+
export { afterAll, describe, expect, test } from './runner';
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { type ContractArtifact, type IamMembershipRpc, type IamTerminalRpc } from '@fabrika/auth-core';
|
|
2
|
+
import type { Database } from '@fabrika/db';
|
|
3
|
+
import { MemoryEmailSender } from '@fabrika/email/memory';
|
|
4
|
+
import type { FileObjectStore } from '@fabrika/platform';
|
|
5
|
+
import { type IamGateway } from '@fabrika/proxy-core';
|
|
6
|
+
import type { z } from 'zod/v4';
|
|
7
|
+
import type { RpcFetch } from '../client/transport';
|
|
8
|
+
import type { AnyContract, DimensionOf } from '../contract/define';
|
|
9
|
+
import type { TokenPurpose } from '../contract/token';
|
|
10
|
+
import type { FILE_SWEEP_JOB } from '../files/kinds';
|
|
11
|
+
import type { FILE_SCAN_JOB, FileScanner } from '../files/scan';
|
|
12
|
+
import { type BootOptions } from '../implement/boot';
|
|
13
|
+
import { type ChallengeFetch } from '../implement/challenge';
|
|
14
|
+
import { type ConnectFetch } from '../implement/connect';
|
|
15
|
+
import type { SpaAssets } from '../implement/dispatch';
|
|
16
|
+
import type { ApplicationEnvironment, ContractImplementation } from '../implement/implement';
|
|
17
|
+
import type { MailKeyScope } from '../jobs/mail';
|
|
18
|
+
import type { HandlerErrorSink } from '../jobs/runner';
|
|
19
|
+
import type { JobRunOptions, JobRunResult } from '../jobs/runner';
|
|
20
|
+
import { type LabelledTestCaller, type LinkTestCaller, type PersonTestCaller, type TestCaller, type TestCallerOptions, type TestGrant, type TestScope } from './caller';
|
|
21
|
+
import { type KitMembership } from './membership';
|
|
22
|
+
export declare const TEST_ISSUER = "https://iam.test";
|
|
23
|
+
export declare const TEST_ENVIRONMENT = "test";
|
|
24
|
+
export interface TestKitOptions {
|
|
25
|
+
/** The application's own variables, as `deps` receives them. */
|
|
26
|
+
readonly env?: ApplicationEnvironment;
|
|
27
|
+
/**
|
|
28
|
+
* The address the kit's proxy reports for every request, as the platform's proxy reports the visitor's
|
|
29
|
+
* (`X-Fabrika-Client-Ip`): what `ctx.client.ip` reads and a per-client bucket counts. `192.0.2.1` by default.
|
|
30
|
+
*/
|
|
31
|
+
readonly clientIp?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The application root an SPA mount's `dir` is relative to; the working directory when omitted. A mount whose built
|
|
34
|
+
* directory exists there is served from it as the deploy serves it: a file by its path, every other path by the
|
|
35
|
+
* directory's `index.html`. A mount not built is an empty HTML document.
|
|
36
|
+
*/
|
|
37
|
+
readonly root?: string;
|
|
38
|
+
/** Serves the SPA mounts in place of their built directories under `root`. */
|
|
39
|
+
readonly assets?: SpaAssets;
|
|
40
|
+
/** The connection a `scoped(tables)` database is bound to, as the runtime adapter supplies it to `boot`. */
|
|
41
|
+
readonly database?: Database;
|
|
42
|
+
/** The object storage of the stored files, as the runtime adapter supplies it to `boot`. */
|
|
43
|
+
readonly files?: FileObjectStore;
|
|
44
|
+
/** The scanner of the scan job of `scan: true` file kinds; without it, the job leaves every such file quarantined. */
|
|
45
|
+
readonly scanner?: FileScanner;
|
|
46
|
+
/**
|
|
47
|
+
* IAM's membership operations in place of the kit's own stand-in: `boot` asks it as the adapter's transport, and the
|
|
48
|
+
* kit's proxy obtains each delegation grant from it. Without it the kit's IAM answers them under IAM's rules.
|
|
49
|
+
*/
|
|
50
|
+
readonly membership?: IamMembershipRpc;
|
|
51
|
+
/**
|
|
52
|
+
* IAM's identifications at a terminal (ADR-0067), for a contract that declares one. Without it the kit's IAM knows no
|
|
53
|
+
* open identification: `inspect` answers null and `assert` `not_found`.
|
|
54
|
+
*/
|
|
55
|
+
readonly terminals?: IamTerminalRpc;
|
|
56
|
+
/**
|
|
57
|
+
* Each import's owner, by the import's name: its origin and edge key, which the platform writes as the import's
|
|
58
|
+
* framework variables, and how a request reaches it — usually the owner kit's `fetch`.
|
|
59
|
+
*/
|
|
60
|
+
readonly connect?: Readonly<Record<string, TestConnection>>;
|
|
61
|
+
/** What answers a public procedure's challenge verification in place of the provider: a stand-in for its siteverify. */
|
|
62
|
+
readonly challengeFetch?: ChallengeFetch;
|
|
63
|
+
/** What a job's handler threw, shown on the developer's own machine: `fabrika app dev` sets it, a test need not. */
|
|
64
|
+
readonly revealHandlerErrors?: HandlerErrorSink;
|
|
65
|
+
/**
|
|
66
|
+
* The stand-in for every job's mail transport; a new `MemoryEmailSender` by default. Pass one with other capabilities
|
|
67
|
+
* to model another provider: `new MemoryEmailSender({ capabilities: { idempotent: false, confirmation: 'queued' } })`.
|
|
68
|
+
* `'none'` models an installation without a transport: `ctx.mail.available` is false, and every send fails
|
|
69
|
+
* `mail_not_configured`.
|
|
70
|
+
*/
|
|
71
|
+
readonly mail?: MemoryEmailSender | 'none';
|
|
72
|
+
/**
|
|
73
|
+
* The origins the environment lists for the contract's endpoints that admit `origins: 'listed'`, as an operator sets them in
|
|
74
|
+
* Control; a page on one of them may call such an endpoint with a key. None when omitted.
|
|
75
|
+
*/
|
|
76
|
+
readonly crossOrigins?: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
export interface TestConnection {
|
|
79
|
+
readonly origin: string;
|
|
80
|
+
readonly key: string;
|
|
81
|
+
readonly fetch: ConnectFetch;
|
|
82
|
+
}
|
|
83
|
+
export interface TestKit<C extends AnyContract> {
|
|
84
|
+
/** The origin requests are resolved against, `https://<app>.test`. */
|
|
85
|
+
readonly origin: string;
|
|
86
|
+
/** The token audience, `<app>@test`. */
|
|
87
|
+
readonly appId: string;
|
|
88
|
+
/** The contract as compiled for `boot`. */
|
|
89
|
+
readonly artifact: ContractArtifact;
|
|
90
|
+
/** A new principal of the test IAM holding `grants`, with a fresh credential; only a link holder has no label. */
|
|
91
|
+
testCaller(grants: readonly TestGrant<C>[], options: TestCallerOptions & {
|
|
92
|
+
readonly kind: 'link';
|
|
93
|
+
}): LinkTestCaller;
|
|
94
|
+
testCaller(grants: readonly TestGrant<C>[], options?: TestCallerOptions & {
|
|
95
|
+
readonly kind?: 'human';
|
|
96
|
+
}): PersonTestCaller;
|
|
97
|
+
testCaller(grants: readonly TestGrant<C>[], options?: TestCallerOptions & {
|
|
98
|
+
readonly kind?: LabelledTestCaller['kind'];
|
|
99
|
+
}): LabelledTestCaller;
|
|
100
|
+
testCaller(grants: readonly TestGrant<C>[], options?: TestCallerOptions): TestCaller;
|
|
101
|
+
/**
|
|
102
|
+
* One value of the contract's dimension `dimension`, for a grant's `scope`. Stored in a variable it keeps its
|
|
103
|
+
* dimension, where an object literal's `type` widens to `string`: `const acme = kit.scope('org', 'acme')`.
|
|
104
|
+
*/
|
|
105
|
+
scope<D extends DimensionOf<C>>(dimension: D, value: string): TestScope<D>;
|
|
106
|
+
/** A request as it reaches the proxy, credentials as written: a script, a `px_` key, or another site. */
|
|
107
|
+
readonly fetch: RpcFetch;
|
|
108
|
+
/** `fetch` from a page of the application: same-origin, with the session cookie of `caller` when given. */
|
|
109
|
+
browser(caller?: TestCaller): RpcFetch;
|
|
110
|
+
/** The test IAM as the proxy calls it: it mints for the credentials `testCaller` issued and serves their key set. */
|
|
111
|
+
readonly iam: IamGateway;
|
|
112
|
+
/**
|
|
113
|
+
* The addressee of an invitation the kit's IAM holds open accepts it, as IAM's onboarding does: `person` holds the
|
|
114
|
+
* invited role in its scope from now on, or, without one, a new person labelled by the invited address does. Answers
|
|
115
|
+
* that person. Throws, leaving the invitation open, as IAM refuses: an external `person` and a role not declared
|
|
116
|
+
* external (`not_eligible`), an inviter that can no longer give the role (`not_allowed`). Throws for an invitation
|
|
117
|
+
* that is not open, and in a kit given its own `membership`.
|
|
118
|
+
*/
|
|
119
|
+
acceptInvitation(invitationId: string, person?: LabelledTestCaller): LabelledTestCaller;
|
|
120
|
+
/**
|
|
121
|
+
* Runs the cron job `name` once, as `fabrika job run <name>` does: under its lease, with the actions IAM signs for
|
|
122
|
+
* its key, its writes fenced. It first waits for the runs a site's `ctx.kick` sent, so it never races them.
|
|
123
|
+
* `FILE_SCAN_JOB` runs the scan of `scan: true` file kinds with the kit's `scanner`, `FILE_SWEEP_JOB` the sweep of
|
|
124
|
+
* unreferenced files. Needs the application's data layer on Postgres.
|
|
125
|
+
*/
|
|
126
|
+
runJob(name: JobName<C> | typeof FILE_SCAN_JOB | typeof FILE_SWEEP_JOB, options?: JobRunOptions): Promise<JobRunResult>;
|
|
127
|
+
/** Waits for the work requests left running: the runs a site's `ctx.kick` sent, a failure's report. */
|
|
128
|
+
drain(): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* The stand-in for every job's transport: what reached it (`accepted`, `handoffs`) and its faults (`refuse`,
|
|
131
|
+
* `unavailable`, `unreachable`, `loseAnswers`, `during`, `heal`). Each message carries the transport key,
|
|
132
|
+
* `transportMailKey(kit.mailKeyScope, key)`. Under `mail: 'none'` nothing reaches it.
|
|
133
|
+
*/
|
|
134
|
+
readonly mail: MemoryEmailSender;
|
|
135
|
+
/** Whose keys this kit's prefix separates: `mailKeyPrefix(kit.mailKeyScope)` is what a job's key gets in front. */
|
|
136
|
+
readonly mailKeyScope: MailKeyScope;
|
|
137
|
+
/**
|
|
138
|
+
* A token of `purpose`, signed with the key the kit handed the application, as a site's `ctx.tokens.sign` signs it:
|
|
139
|
+
* what a test sends where an input carries `signedToken('<purpose>')`.
|
|
140
|
+
*/
|
|
141
|
+
signToken<const P extends TokenPurposeName<C>>(purpose: P, claims: TokenClaimsInput<C, P>, options: {
|
|
142
|
+
readonly days: number;
|
|
143
|
+
}): string;
|
|
144
|
+
/**
|
|
145
|
+
* A solved ALTCHA challenge of `procedure`, a procedure of a client over this kit whose challenge is
|
|
146
|
+
* `provider: 'altcha'`: what the page sends as `challengeToken`. Each one passes once. The kit issues the cheapest
|
|
147
|
+
* challenge whatever the contract's `cost`, so solving one takes milliseconds.
|
|
148
|
+
*/
|
|
149
|
+
solveChallenge(procedure: object): Promise<string>;
|
|
150
|
+
}
|
|
151
|
+
/** The names of the contract's cron jobs. */
|
|
152
|
+
export type JobName<C> = C extends {
|
|
153
|
+
readonly surface: {
|
|
154
|
+
readonly jobs: infer J;
|
|
155
|
+
};
|
|
156
|
+
} ? keyof J & string : never;
|
|
157
|
+
type TokensOf<C> = C extends {
|
|
158
|
+
readonly surface: {
|
|
159
|
+
readonly tokens: infer T;
|
|
160
|
+
};
|
|
161
|
+
} ? T : Record<never, never>;
|
|
162
|
+
/** The token purposes of the contract's kit. */
|
|
163
|
+
export type TokenPurposeName<C> = keyof TokensOf<C> & string;
|
|
164
|
+
/** What a token of purpose `P` carries, as a site passes it to `sign`. */
|
|
165
|
+
export type TokenClaimsInput<C, P extends string> = TokensOf<C> extends {
|
|
166
|
+
readonly [K in P]: TokenPurpose<infer Claims>;
|
|
167
|
+
} ? z.input<Claims> : never;
|
|
168
|
+
/** Boots `app` the way its generated entrypoint would, behind the test proxy and IAM. Throws `BootError` like `boot`. */
|
|
169
|
+
export declare function testKit<C extends AnyContract>(app: ContractImplementation<C>, options?: TestKitOptions): TestKit<C>;
|
|
170
|
+
/** The kit of the generated tests, and its stand-in IAM, which seeds the invitations and links a case names. */
|
|
171
|
+
export interface StandInIamKit<C extends AnyContract> {
|
|
172
|
+
readonly kit: TestKit<C>;
|
|
173
|
+
readonly standIn: KitMembership;
|
|
174
|
+
}
|
|
175
|
+
/** The kit of the generated tests, with a handle on its stand-in IAM to seed what a case names. */
|
|
176
|
+
export declare function standInIamKit<C extends AnyContract>(app: ContractImplementation<C>, options?: Omit<TestKitOptions, 'membership'>): StandInIamKit<C>;
|
|
177
|
+
/** A booted kit, and its stand-in IAM unless it was given a membership transport. */
|
|
178
|
+
export interface BootedTestKit<C extends AnyContract> {
|
|
179
|
+
readonly kit: TestKit<C>;
|
|
180
|
+
readonly standIn: KitMembership | null;
|
|
181
|
+
/** The caller `value` is when `testCaller` or `acceptInvitation` of this kit made it; `undefined` for any other value. */
|
|
182
|
+
callerOf(value: unknown): TestCaller | undefined;
|
|
183
|
+
}
|
|
184
|
+
/** What only `fabrika app dev`'s kit boots with, never a test's. */
|
|
185
|
+
export type DevBootOptions = Pick<BootOptions, 'repeatableChallengeToken'>;
|
|
186
|
+
export declare function bootTestKit<C extends AnyContract>(app: ContractImplementation<C>, options: TestKitOptions, dev?: DevBootOptions): BootedTestKit<C>;
|
|
187
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type ContractArtifact, type IamMembershipRpc, type MemberTarget, type PermissionEntry, type RevokeMemberInput, type Scope } from '@fabrika/auth-core';
|
|
2
|
+
import type { TestCaller, TestScope } from './caller';
|
|
3
|
+
/** A role a test caller holds in one scope value, or globally (`null`). */
|
|
4
|
+
export interface TestMembership {
|
|
5
|
+
readonly role: string;
|
|
6
|
+
readonly scope: TestScope | null;
|
|
7
|
+
}
|
|
8
|
+
export interface DirectoryCallers {
|
|
9
|
+
/**
|
|
10
|
+
* The caller a membership request's credential (its token or `px_` key) belongs to, as IAM verifies it for `app`:
|
|
11
|
+
* none for a credential of another application or an expired token.
|
|
12
|
+
*/
|
|
13
|
+
byCredential(app: string, credential: string): TestCaller | undefined;
|
|
14
|
+
/** The job a link operation's credential (its workload key or token) belongs to, as IAM resolves it for `app`. */
|
|
15
|
+
jobByCredential(app: string, credential: string): KitJobCaller | undefined;
|
|
16
|
+
/** Every caller with its role grants. */
|
|
17
|
+
all(): readonly {
|
|
18
|
+
readonly caller: TestCaller;
|
|
19
|
+
readonly memberships: readonly TestMembership[];
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
/** A job's principal, as IAM holds it to its declaration: its actions app-wide, and the one link mount it issues. */
|
|
23
|
+
export interface KitJobCaller {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly actions: readonly string[];
|
|
26
|
+
readonly issues: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** A job's workload key (`expiresAt: null`) or a token the kit signed for it. */
|
|
29
|
+
export interface JobCredential {
|
|
30
|
+
readonly job: KitJobCaller;
|
|
31
|
+
readonly expiresAt: number | null;
|
|
32
|
+
}
|
|
33
|
+
/** A write `kitMembership` allowed, by the principal that asked; the credential and the delegation grant are left out. */
|
|
34
|
+
export type AskedWrite = {
|
|
35
|
+
readonly operation: 'createScope';
|
|
36
|
+
readonly principalId: string;
|
|
37
|
+
readonly scope: Scope;
|
|
38
|
+
readonly role: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly operation: 'assignMember';
|
|
41
|
+
readonly principalId: string;
|
|
42
|
+
readonly scope: Scope;
|
|
43
|
+
readonly role: string;
|
|
44
|
+
readonly member: MemberTarget;
|
|
45
|
+
/** The invitation it answered for an address; `null` for an existing principal. */
|
|
46
|
+
readonly invitationId: string | null;
|
|
47
|
+
} | {
|
|
48
|
+
readonly operation: 'revokeMember';
|
|
49
|
+
readonly principalId: string;
|
|
50
|
+
readonly scope: Scope;
|
|
51
|
+
readonly member: RevokeMemberInput['member'];
|
|
52
|
+
} | {
|
|
53
|
+
readonly operation: 'issueLink';
|
|
54
|
+
readonly principalId: string;
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly mount: string;
|
|
57
|
+
readonly chain: readonly Scope[];
|
|
58
|
+
} | {
|
|
59
|
+
readonly operation: 'revokeLink';
|
|
60
|
+
readonly principalId: string;
|
|
61
|
+
readonly id: string;
|
|
62
|
+
};
|
|
63
|
+
/** An invitation its addressee accepted: the role it gave, in its scope, to the invited address. */
|
|
64
|
+
export interface AcceptedInvitation {
|
|
65
|
+
readonly scope: Scope;
|
|
66
|
+
readonly role: string;
|
|
67
|
+
readonly email: string;
|
|
68
|
+
}
|
|
69
|
+
export interface KitMembership {
|
|
70
|
+
readonly rpc: IamMembershipRpc;
|
|
71
|
+
/** Every write it allowed, in order. */
|
|
72
|
+
readonly asked: readonly AskedWrite[];
|
|
73
|
+
/** The permissions IAM signs for `caller` now: the test's grants, with the roles this stand-in assigned or revoked. */
|
|
74
|
+
permsOf(caller: TestCaller): readonly PermissionEntry[];
|
|
75
|
+
/**
|
|
76
|
+
* Closes the open invitation `invitationId` as accepted, as IAM's onboarding does, and answers what it gives. With
|
|
77
|
+
* `recipient`, that person holds the role from now on; without one the kit makes the new person who holds it. As IAM,
|
|
78
|
+
* it refuses an external recipient of a role not declared external (`not_eligible`) and an invitation whose inviter
|
|
79
|
+
* could no longer give the role (`not_allowed`), and then leaves the invitation open.
|
|
80
|
+
*/
|
|
81
|
+
accept(invitationId: string, recipient?: TestCaller): AcceptedInvitation;
|
|
82
|
+
/** An open invitation of `role` in `scope` to `email`, as an earlier request opened it; answers its id. */
|
|
83
|
+
seedInvitation(scope: Scope, role: string, email: string): string;
|
|
84
|
+
/** A share link of `mount` bound to `chain`, as `issuer` issued it in an earlier request; answers its id. */
|
|
85
|
+
seedLink(issuer: string, mount: string, chain: readonly Scope[]): string;
|
|
86
|
+
/** The link a key it issued is bound to, as IAM resolves a link key; `undefined` for any other key and a revoked link. */
|
|
87
|
+
linkOfKey(key: string): IssuedLinkKey | undefined;
|
|
88
|
+
/** The roles `caller` holds now: the test's grants, with the ones this stand-in assigned or revoked. */
|
|
89
|
+
membershipsOf(caller: TestCaller): readonly TestMembership[];
|
|
90
|
+
/** Every invitation it opened, the seeded ones included, in the order they were first opened. */
|
|
91
|
+
invitations(): readonly SentInvitation[];
|
|
92
|
+
}
|
|
93
|
+
/** A share link the stand-in issued, as its key resolves. */
|
|
94
|
+
export interface IssuedLinkKey {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
readonly mount: string;
|
|
97
|
+
readonly chain: readonly Scope[];
|
|
98
|
+
}
|
|
99
|
+
/** The invitation e-mail IAM would have sent: to whom, for which role in which scope, and where it stands now. */
|
|
100
|
+
export interface SentInvitation {
|
|
101
|
+
readonly id: string;
|
|
102
|
+
readonly email: string;
|
|
103
|
+
readonly role: string;
|
|
104
|
+
readonly scope: Scope;
|
|
105
|
+
readonly expiresAt: number;
|
|
106
|
+
/** When it was last sent, in seconds: when it was opened, or its latest resend. */
|
|
107
|
+
readonly sentAt: number;
|
|
108
|
+
readonly status: 'open' | 'accepted' | 'cancelled' | 'expired';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The addresses the organization names, as IAM's configuration does (its human e-mails, its e-mail domains): IAM never
|
|
112
|
+
* invites one to an external role (`internal_mailbox`).
|
|
113
|
+
*/
|
|
114
|
+
export interface OrganizationMailboxes {
|
|
115
|
+
readonly emails: readonly string[];
|
|
116
|
+
readonly domains: readonly string[];
|
|
117
|
+
}
|
|
118
|
+
/** A token the kit's IAM signed, for the caller it was minted for, until its `exp`. */
|
|
119
|
+
export interface MintedToken {
|
|
120
|
+
readonly caller: TestCaller;
|
|
121
|
+
readonly expiresAt: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The kit's callers as IAM resolves a membership request's credential for `app`: a `px_` key the kit issued, or a token
|
|
125
|
+
* it signed that has not expired; nothing for another application.
|
|
126
|
+
*/
|
|
127
|
+
export declare function credentialCallers(app: string, keys: ReadonlyMap<string, TestCaller>, tokens: ReadonlyMap<string, MintedToken>, memberships: () => ReturnType<DirectoryCallers['all']>, jobs?: ReadonlyMap<string, JobCredential>): DirectoryCallers;
|
|
128
|
+
/**
|
|
129
|
+
* The directory of the kit's callers, and every write checked against the caller's grants as IAM checks it.
|
|
130
|
+
* `organization` names the mailboxes IAM's configuration would; by default none.
|
|
131
|
+
*/
|
|
132
|
+
export declare function kitMembership(artifact: ContractArtifact, callers: DirectoryCallers, organization?: OrganizationMailboxes): KitMembership;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type Tables } from '@fabrika/db';
|
|
2
|
+
import { type TestDatabase } from '@fabrika/db/testing';
|
|
3
|
+
import { type AnyContract } from '../contract/define';
|
|
4
|
+
/** Which of the two seeded rows a request names: the caller's own, or the one in the scope it holds nothing in. */
|
|
5
|
+
export type IsolationRow = 'own' | 'other';
|
|
6
|
+
/** A child row addressed through the caller's own parent with the other row's key: a key from another scope. */
|
|
7
|
+
export type ChildIsolationRow = IsolationRow | 'crossed';
|
|
8
|
+
export interface IsolationOutcome {
|
|
9
|
+
/** 200 when the operation ran, otherwise the status the caller got. */
|
|
10
|
+
readonly status: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ReadOutcome extends IsolationOutcome {
|
|
13
|
+
/** The columns the row came back with. */
|
|
14
|
+
readonly columns: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
export interface ListOutcome extends IsolationOutcome {
|
|
17
|
+
/** Which seeded rows the list reached. */
|
|
18
|
+
readonly rows: readonly IsolationRow[];
|
|
19
|
+
}
|
|
20
|
+
export interface CountOutcome extends IsolationOutcome {
|
|
21
|
+
readonly count: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AppCountOutcome extends IsolationOutcome {
|
|
24
|
+
/** How many more rows the count answered after the seed than before it. */
|
|
25
|
+
readonly counted: number;
|
|
26
|
+
}
|
|
27
|
+
export interface MisuseOutcome extends IsolationOutcome {
|
|
28
|
+
/** Whether the data layer refused the read capability (`CapabilityError`). */
|
|
29
|
+
readonly refused: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface PolicyIsolationKit {
|
|
32
|
+
readonly table: string;
|
|
33
|
+
/** The dimension the caller's grants are scoped by. */
|
|
34
|
+
readonly dimension: string;
|
|
35
|
+
get(row: ChildIsolationRow): Promise<ReadOutcome>;
|
|
36
|
+
/** Lists through a `Within` of the caller's scope. */
|
|
37
|
+
list(): Promise<ListOutcome>;
|
|
38
|
+
/** A child table listed through the parent's `Ref`; the dimension table has no parent. */
|
|
39
|
+
listByParent(row: IsolationRow): Promise<ListOutcome>;
|
|
40
|
+
count(): Promise<CountOutcome>;
|
|
41
|
+
insert(into: IsolationRow): Promise<IsolationOutcome>;
|
|
42
|
+
update(row: ChildIsolationRow): Promise<IsolationOutcome>;
|
|
43
|
+
remove(row: ChildIsolationRow): Promise<IsolationOutcome>;
|
|
44
|
+
/** A `Ref` for the read action handed to update: the data layer must refuse it. */
|
|
45
|
+
updateWithRead(): Promise<MisuseOutcome>;
|
|
46
|
+
/** A `Ref` for the read action handed to remove. */
|
|
47
|
+
removeWithRead(): Promise<MisuseOutcome>;
|
|
48
|
+
/** Lists from an `app` endpoint of `action`, one of the table's `appRead`, for a global holder: every row. */
|
|
49
|
+
appList(action: string): Promise<ListOutcome>;
|
|
50
|
+
/**
|
|
51
|
+
* Counts from an `app` endpoint of `action`, one of the table's `appCount`, for a global holder, before and after
|
|
52
|
+
* two rows of two scopes are seeded: `counted` is 2 when the count reaches every row.
|
|
53
|
+
*/
|
|
54
|
+
appCount(action: string): Promise<AppCountOutcome>;
|
|
55
|
+
/** Lists from an `app` endpoint of `action`, one of the table's `appCount`: the data layer must refuse it. */
|
|
56
|
+
appListWithCount(action: string): Promise<MisuseOutcome>;
|
|
57
|
+
/**
|
|
58
|
+
* Lists from an `app` endpoint of `action`, one of the table's `appRead`: `leaked` names each answered column that
|
|
59
|
+
* declares another read action, `missing` each column that declares exactly `action` and was not answered.
|
|
60
|
+
*/
|
|
61
|
+
appColumns(action: string): Promise<AppColumnsOutcome>;
|
|
62
|
+
/**
|
|
63
|
+
* Groups by the key and every column from an `app` endpoint of `action`, one of the table's `appCount`: a group is a
|
|
64
|
+
* row's values, so the data layer must refuse it.
|
|
65
|
+
*/
|
|
66
|
+
appGroupWithCount(action: string): Promise<MisuseOutcome>;
|
|
67
|
+
}
|
|
68
|
+
export interface AppColumnsOutcome extends IsolationOutcome {
|
|
69
|
+
readonly leaked: readonly string[];
|
|
70
|
+
readonly missing: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* An application table's isolation kit: the rows belong to no scope, so the boundary is the grant. `global` holds the
|
|
74
|
+
* table's actions as global grants; `scoped` holds the same actions in one value of a dimension only, which the proxy
|
|
75
|
+
* refuses on an `app` endpoint. Each attempt seeds one fresh row.
|
|
76
|
+
*/
|
|
77
|
+
export interface AppIsolationKit {
|
|
78
|
+
readonly table: string;
|
|
79
|
+
get(caller: AppCaller): Promise<ReadOutcome>;
|
|
80
|
+
/** Lists through the `app` capability; `rows` is `['own']` when the list holds the seeded row. */
|
|
81
|
+
list(caller: AppCaller): Promise<ListOutcome>;
|
|
82
|
+
insert(caller: AppCaller): Promise<IsolationOutcome>;
|
|
83
|
+
update(caller: AppCaller): Promise<IsolationOutcome>;
|
|
84
|
+
remove(caller: AppCaller): Promise<IsolationOutcome>;
|
|
85
|
+
/** The capability of the read action handed to update: the data layer must refuse it. */
|
|
86
|
+
updateWithRead(): Promise<MisuseOutcome>;
|
|
87
|
+
/** The capability of the read action handed to remove. */
|
|
88
|
+
removeWithRead(): Promise<MisuseOutcome>;
|
|
89
|
+
/**
|
|
90
|
+
* Lists from an `over` endpoint of the table's read action, for a caller who holds it in one scope only: a scoped
|
|
91
|
+
* site reads every row of an application table by its read action.
|
|
92
|
+
*/
|
|
93
|
+
scopedList(): Promise<ListOutcome>;
|
|
94
|
+
/** The same list from an `over` endpoint of another action the table names: the data layer must refuse it. */
|
|
95
|
+
scopedListWithOther(): Promise<MisuseOutcome>;
|
|
96
|
+
}
|
|
97
|
+
export type AppCaller = 'global' | 'scoped';
|
|
98
|
+
/**
|
|
99
|
+
* The isolation kit of `table`, an application table of `tables`, over `postgres`, through a probe contract of `app`
|
|
100
|
+
* endpoints. `contract` gives only the vocabulary. The kit binds a data layer of its own.
|
|
101
|
+
*/
|
|
102
|
+
export declare function appPolicyIsolationKit<C extends AnyContract, DB, P>(contract: C, tables: Tables<DB, P>, table: string, postgres: TestDatabase): AppIsolationKit;
|
|
103
|
+
/**
|
|
104
|
+
* The policy isolation kit of `table`, a dimension or child table of `tables`, over `postgres`. `contract` gives only the
|
|
105
|
+
* vocabulary: its actions, dimensions and roles. The kit binds a data layer of its own, so it does not touch the
|
|
106
|
+
* application's.
|
|
107
|
+
*/
|
|
108
|
+
export declare function policyIsolationKit<C extends AnyContract, DB, P>(contract: C, tables: Tables<DB, P>, table: string, postgres: TestDatabase): PolicyIsolationKit;
|
|
109
|
+
/**
|
|
110
|
+
* The scope type a `scoped` caller's grants name: the vocabulary's first dimension, or, in a vocabulary without one, a
|
|
111
|
+
* name IAM never grants in. Either way the grant is not global, which is all the app form looks at.
|
|
112
|
+
*/
|
|
113
|
+
export declare function scopeTypeOf(dimensions: object): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ContractArtifact } from '@fabrika/auth-core';
|
|
2
|
+
import { type DelegationIssuer, type IamGateway, type TokenCache } from '@fabrika/proxy-core';
|
|
3
|
+
/** What the proxy did with a request: answered it itself, or forwarded it with what only the proxy may set. */
|
|
4
|
+
export type ProxyOutcome = {
|
|
5
|
+
readonly outcome: 'forward';
|
|
6
|
+
/**
|
|
7
|
+
* Every header only the proxy may assert, with the value the decision gave it. A credentialed request that carried
|
|
8
|
+
* `Authorization` has it replaced by the minted token, so the caller's `px_` key never reaches the application.
|
|
9
|
+
*/
|
|
10
|
+
readonly asserted: ReadonlyMap<string, string>;
|
|
11
|
+
/** Whether the request's own `Authorization` reaches the application: only on a webhook row the decision marked to pass it on. */
|
|
12
|
+
readonly keepsAuthorization: boolean;
|
|
13
|
+
/** What the application's answer is given for an admitted cross-origin request, over its own headers. */
|
|
14
|
+
readonly decorate: (headers: Headers) => void;
|
|
15
|
+
} | {
|
|
16
|
+
readonly outcome: 'answer';
|
|
17
|
+
readonly response: Response;
|
|
18
|
+
};
|
|
19
|
+
/** What the proxy reads of a request; its headers have already lost every one a client may not assert. */
|
|
20
|
+
export interface ProxiedRequest {
|
|
21
|
+
readonly method: string;
|
|
22
|
+
readonly url: string;
|
|
23
|
+
readonly headers: Headers;
|
|
24
|
+
}
|
|
25
|
+
export type TestProxy = (request: ProxiedRequest) => Promise<ProxyOutcome>;
|
|
26
|
+
export interface EndpointProxyOptions {
|
|
27
|
+
readonly artifact: ContractArtifact;
|
|
28
|
+
readonly appId: string;
|
|
29
|
+
readonly host: string;
|
|
30
|
+
readonly iam: IamGateway;
|
|
31
|
+
readonly issuer: string;
|
|
32
|
+
/** Issues the delegation grants of membership and scope-creating endpoints, as IAM does for the proxy. */
|
|
33
|
+
readonly delegations: DelegationIssuer | null;
|
|
34
|
+
/** The proxy's token cache, as production keeps one; omitted, every request mints. */
|
|
35
|
+
readonly cache?: TokenCache;
|
|
36
|
+
/** The origins the environment lists for the contract's cross-origin endpoints; none when omitted. */
|
|
37
|
+
readonly crossOrigins?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
/** The production proxy: the verify service over the contract's endpoint table, asked the way an enforcement point asks it. */
|
|
40
|
+
export declare function endpointProxy(options: EndpointProxyOptions): TestProxy;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as bunTest from 'bun:test';
|
|
2
|
+
/** Captures Bun's test functions process-wide, once; the platform's preload calls it before the application loads. */
|
|
3
|
+
export declare function installTestRunner(): void;
|
|
4
|
+
export declare function describe(name: string, body: () => void): void;
|
|
5
|
+
/** Registers a case that fails unless its body ran at least one `expect`. */
|
|
6
|
+
export declare function test(name: string, body: () => Promise<void>): void;
|
|
7
|
+
export declare function expect<T>(value: T): ReturnType<typeof bunTest.expect<T>>;
|
|
8
|
+
export declare function afterAll(body: () => Promise<void>): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** A file of the first of `types` the kit can write; `name` (a collection or a kind) names it in the error when none is. */
|
|
2
|
+
export declare function sampleFile(name: string, types: readonly string[]): {
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
5
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The status the application refused a call with; a failure that is not its answer (no status) is rethrown. */
|
|
2
|
+
export declare function refusalStatus(error: unknown): number;
|
|
3
|
+
/** A call's status: 200 when it resolves, the status the application refused it with otherwise. */
|
|
4
|
+
export declare function statusOf(call: Promise<unknown>): Promise<number>;
|