@fabrika/app 0.9.0-dev.1 → 0.9.0-dev.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 +27 -26
- package/skill/SKILL.md +1 -1
- package/skill/references/app-setup.md +8 -8
- package/src/dev/changes.ts +5 -43
- package/src/dev/contract-digest.ts +17 -0
- package/src/dev/index.ts +1 -1
- package/src/dev/launcher.ts +69 -22
- package/src/dev/wire.ts +2 -1
- package/types/src/agent-skill.d.ts +29 -0
- package/types/src/build.d.ts +11 -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.d.ts +49 -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 +501 -0
- package/types/src/contract/declaration.d.ts +303 -0
- package/types/src/contract/defaults.d.ts +208 -0
- package/types/src/contract/define.d.ts +359 -0
- package/types/src/contract/file.d.ts +44 -0
- package/types/src/contract/group.d.ts +274 -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 +210 -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 +30 -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 +22 -0
- package/types/src/dev/wire.d.ts +122 -0
- package/types/src/disk-assets.d.ts +3 -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/kinds.d.ts +105 -0
- package/types/src/files/scan.d.ts +80 -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 +26 -0
- package/types/src/implement/attachment.d.ts +14 -0
- package/types/src/implement/boot.d.ts +109 -0
- package/types/src/implement/bounded.d.ts +22 -0
- package/types/src/implement/browser-config.d.ts +19 -0
- package/types/src/implement/capability.d.ts +140 -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 +218 -0
- package/types/src/implement/egress.d.ts +14 -0
- package/types/src/implement/environment.d.ts +14 -0
- package/types/src/implement/failures.d.ts +34 -0
- package/types/src/implement/faults.d.ts +44 -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 +24 -0
- package/types/src/implement/invocation.d.ts +120 -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 +52 -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/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 +145 -0
- package/types/src/jobs/schedule.d.ts +15 -0
- package/types/src/jobs/scrub.d.ts +12 -0
- package/types/src/jobs/types.d.ts +92 -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/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 +99 -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 +182 -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,182 @@
|
|
|
1
|
+
import { type ContractArtifact, type IamMembershipRpc } 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
|
+
* Each import's owner, by the import's name: its origin and edge key, which the platform writes as the import's
|
|
53
|
+
* framework variables, and how a request reaches it — usually the owner kit's `fetch`.
|
|
54
|
+
*/
|
|
55
|
+
readonly connect?: Readonly<Record<string, TestConnection>>;
|
|
56
|
+
/** What answers a public procedure's challenge verification in place of the provider: a stand-in for its siteverify. */
|
|
57
|
+
readonly challengeFetch?: ChallengeFetch;
|
|
58
|
+
/** What a job's handler threw, shown on the developer's own machine: `fabrika app dev` sets it, a test need not. */
|
|
59
|
+
readonly revealHandlerErrors?: HandlerErrorSink;
|
|
60
|
+
/**
|
|
61
|
+
* The stand-in for every job's mail transport; a new `MemoryEmailSender` by default. Pass one with other capabilities
|
|
62
|
+
* to model another provider: `new MemoryEmailSender({ capabilities: { idempotent: false, confirmation: 'queued' } })`.
|
|
63
|
+
* `'none'` models an installation without a transport: `ctx.mail.available` is false, and every send fails
|
|
64
|
+
* `mail_not_configured`.
|
|
65
|
+
*/
|
|
66
|
+
readonly mail?: MemoryEmailSender | 'none';
|
|
67
|
+
/**
|
|
68
|
+
* The origins the environment lists for the contract's endpoints that admit `origins: 'listed'`, as an operator sets them in
|
|
69
|
+
* Control; a page on one of them may call such an endpoint with a key. None when omitted.
|
|
70
|
+
*/
|
|
71
|
+
readonly crossOrigins?: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
export interface TestConnection {
|
|
74
|
+
readonly origin: string;
|
|
75
|
+
readonly key: string;
|
|
76
|
+
readonly fetch: ConnectFetch;
|
|
77
|
+
}
|
|
78
|
+
export interface TestKit<C extends AnyContract> {
|
|
79
|
+
/** The origin requests are resolved against, `https://<app>.test`. */
|
|
80
|
+
readonly origin: string;
|
|
81
|
+
/** The token audience, `<app>@test`. */
|
|
82
|
+
readonly appId: string;
|
|
83
|
+
/** The contract as compiled for `boot`. */
|
|
84
|
+
readonly artifact: ContractArtifact;
|
|
85
|
+
/** A new principal of the test IAM holding `grants`, with a fresh credential; only a link holder has no label. */
|
|
86
|
+
testCaller(grants: readonly TestGrant<C>[], options: TestCallerOptions & {
|
|
87
|
+
readonly kind: 'link';
|
|
88
|
+
}): LinkTestCaller;
|
|
89
|
+
testCaller(grants: readonly TestGrant<C>[], options?: TestCallerOptions & {
|
|
90
|
+
readonly kind?: 'human';
|
|
91
|
+
}): PersonTestCaller;
|
|
92
|
+
testCaller(grants: readonly TestGrant<C>[], options?: TestCallerOptions & {
|
|
93
|
+
readonly kind?: LabelledTestCaller['kind'];
|
|
94
|
+
}): LabelledTestCaller;
|
|
95
|
+
testCaller(grants: readonly TestGrant<C>[], options?: TestCallerOptions): TestCaller;
|
|
96
|
+
/**
|
|
97
|
+
* One value of the contract's dimension `dimension`, for a grant's `scope`. Stored in a variable it keeps its
|
|
98
|
+
* dimension, where an object literal's `type` widens to `string`: `const acme = kit.scope('org', 'acme')`.
|
|
99
|
+
*/
|
|
100
|
+
scope<D extends DimensionOf<C>>(dimension: D, value: string): TestScope<D>;
|
|
101
|
+
/** A request as it reaches the proxy, credentials as written: a script, a `px_` key, or another site. */
|
|
102
|
+
readonly fetch: RpcFetch;
|
|
103
|
+
/** `fetch` from a page of the application: same-origin, with the session cookie of `caller` when given. */
|
|
104
|
+
browser(caller?: TestCaller): RpcFetch;
|
|
105
|
+
/** The test IAM as the proxy calls it: it mints for the credentials `testCaller` issued and serves their key set. */
|
|
106
|
+
readonly iam: IamGateway;
|
|
107
|
+
/**
|
|
108
|
+
* The addressee of an invitation the kit's IAM holds open accepts it, as IAM's onboarding does: `person` holds the
|
|
109
|
+
* invited role in its scope from now on, or, without one, a new person labelled by the invited address does. Answers
|
|
110
|
+
* that person. Throws, leaving the invitation open, as IAM refuses: an external `person` and a role not declared
|
|
111
|
+
* external (`not_eligible`), an inviter that can no longer give the role (`not_allowed`). Throws for an invitation
|
|
112
|
+
* that is not open, and in a kit given its own `membership`.
|
|
113
|
+
*/
|
|
114
|
+
acceptInvitation(invitationId: string, person?: LabelledTestCaller): LabelledTestCaller;
|
|
115
|
+
/**
|
|
116
|
+
* Runs the cron job `name` once, as `fabrika job run <name>` does: under its lease, with the actions IAM signs for
|
|
117
|
+
* its key, its writes fenced. It first waits for the runs a site's `ctx.kick` sent, so it never races them.
|
|
118
|
+
* `FILE_SCAN_JOB` runs the scan of `scan: true` file kinds with the kit's `scanner`, `FILE_SWEEP_JOB` the sweep of
|
|
119
|
+
* unreferenced files. Needs the application's data layer on Postgres.
|
|
120
|
+
*/
|
|
121
|
+
runJob(name: JobName<C> | typeof FILE_SCAN_JOB | typeof FILE_SWEEP_JOB, options?: JobRunOptions): Promise<JobRunResult>;
|
|
122
|
+
/** Waits for the work requests left running: the runs a site's `ctx.kick` sent, a failure's report. */
|
|
123
|
+
drain(): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* The stand-in for every job's transport: what reached it (`accepted`, `handoffs`) and its faults (`refuse`,
|
|
126
|
+
* `unavailable`, `unreachable`, `loseAnswers`, `during`, `heal`). Each message carries the transport key,
|
|
127
|
+
* `transportMailKey(kit.mailKeyScope, key)`. Under `mail: 'none'` nothing reaches it.
|
|
128
|
+
*/
|
|
129
|
+
readonly mail: MemoryEmailSender;
|
|
130
|
+
/** Whose keys this kit's prefix separates: `mailKeyPrefix(kit.mailKeyScope)` is what a job's key gets in front. */
|
|
131
|
+
readonly mailKeyScope: MailKeyScope;
|
|
132
|
+
/**
|
|
133
|
+
* A token of `purpose`, signed with the key the kit handed the application, as a site's `ctx.tokens.sign` signs it:
|
|
134
|
+
* what a test sends where an input carries `signedToken('<purpose>')`.
|
|
135
|
+
*/
|
|
136
|
+
signToken<const P extends TokenPurposeName<C>>(purpose: P, claims: TokenClaimsInput<C, P>, options: {
|
|
137
|
+
readonly days: number;
|
|
138
|
+
}): string;
|
|
139
|
+
/**
|
|
140
|
+
* A solved ALTCHA challenge of `procedure`, a procedure of a client over this kit whose challenge is
|
|
141
|
+
* `provider: 'altcha'`: what the page sends as `challengeToken`. Each one passes once. The kit issues the cheapest
|
|
142
|
+
* challenge whatever the contract's `cost`, so solving one takes milliseconds.
|
|
143
|
+
*/
|
|
144
|
+
solveChallenge(procedure: object): Promise<string>;
|
|
145
|
+
}
|
|
146
|
+
/** The names of the contract's cron jobs. */
|
|
147
|
+
export type JobName<C> = C extends {
|
|
148
|
+
readonly surface: {
|
|
149
|
+
readonly jobs: infer J;
|
|
150
|
+
};
|
|
151
|
+
} ? keyof J & string : never;
|
|
152
|
+
type TokensOf<C> = C extends {
|
|
153
|
+
readonly surface: {
|
|
154
|
+
readonly tokens: infer T;
|
|
155
|
+
};
|
|
156
|
+
} ? T : Record<never, never>;
|
|
157
|
+
/** The token purposes of the contract's kit. */
|
|
158
|
+
export type TokenPurposeName<C> = keyof TokensOf<C> & string;
|
|
159
|
+
/** What a token of purpose `P` carries, as a site passes it to `sign`. */
|
|
160
|
+
export type TokenClaimsInput<C, P extends string> = TokensOf<C> extends {
|
|
161
|
+
readonly [K in P]: TokenPurpose<infer Claims>;
|
|
162
|
+
} ? z.input<Claims> : never;
|
|
163
|
+
/** Boots `app` the way its generated entrypoint would, behind the test proxy and IAM. Throws `BootError` like `boot`. */
|
|
164
|
+
export declare function testKit<C extends AnyContract>(app: ContractImplementation<C>, options?: TestKitOptions): TestKit<C>;
|
|
165
|
+
/** The kit of the generated tests, and its stand-in IAM, which seeds the invitations and links a case names. */
|
|
166
|
+
export interface StandInIamKit<C extends AnyContract> {
|
|
167
|
+
readonly kit: TestKit<C>;
|
|
168
|
+
readonly standIn: KitMembership;
|
|
169
|
+
}
|
|
170
|
+
/** The kit of the generated tests, with a handle on its stand-in IAM to seed what a case names. */
|
|
171
|
+
export declare function standInIamKit<C extends AnyContract>(app: ContractImplementation<C>, options?: Omit<TestKitOptions, 'membership'>): StandInIamKit<C>;
|
|
172
|
+
/** A booted kit, and its stand-in IAM unless it was given a membership transport. */
|
|
173
|
+
export interface BootedTestKit<C extends AnyContract> {
|
|
174
|
+
readonly kit: TestKit<C>;
|
|
175
|
+
readonly standIn: KitMembership | null;
|
|
176
|
+
/** The caller `value` is when `testCaller` or `acceptInvitation` of this kit made it; `undefined` for any other value. */
|
|
177
|
+
callerOf(value: unknown): TestCaller | undefined;
|
|
178
|
+
}
|
|
179
|
+
/** What only `fabrika app dev`'s kit boots with, never a test's. */
|
|
180
|
+
export type DevBootOptions = Pick<BootOptions, 'repeatableChallengeToken'>;
|
|
181
|
+
export declare function bootTestKit<C extends AnyContract>(app: ContractImplementation<C>, options: TestKitOptions, dev?: DevBootOptions): BootedTestKit<C>;
|
|
182
|
+
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
|
+
/** Every header only the proxy may assert, with the value the decision gave it. */
|
|
7
|
+
readonly asserted: ReadonlyMap<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the request's own `Authorization` reaches the application: not on an anonymous endpoint, except a webhook
|
|
10
|
+
* row the decision marked to pass it on.
|
|
11
|
+
*/
|
|
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>;
|