@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,120 @@
|
|
|
1
|
+
import { type ContractArtifact, type JobMailer } from '@fabrika/auth-core';
|
|
2
|
+
import { type DataAccess, type DataHandles, type FileBinding, type Session, type SqlTag } from '@fabrika/db/framework';
|
|
3
|
+
import type { DataKind, UnscopedMode } from '@fabrika/db/tables';
|
|
4
|
+
import type { SqlMode } from '../contract/context';
|
|
5
|
+
import { type RequestFrame } from './capability';
|
|
6
|
+
/** A site's declared off-road reach, parsed at `boot`: what its handler's `ctx` gets beyond the paved road. */
|
|
7
|
+
export interface SiteDeclaration {
|
|
8
|
+
readonly sql: SqlMode | null;
|
|
9
|
+
readonly unscoped: Readonly<Record<string, UnscopedMode>>;
|
|
10
|
+
readonly connect: readonly string[];
|
|
11
|
+
readonly issues: string | null;
|
|
12
|
+
readonly mailer: JobMailer | null;
|
|
13
|
+
/** The cron jobs `ctx.kick` may name. */
|
|
14
|
+
readonly kicks: readonly string[];
|
|
15
|
+
/** The webhook whose bindings `ctx.webhooks` creates and removes. */
|
|
16
|
+
readonly binds: string | null;
|
|
17
|
+
/** The token purposes `ctx.tokens.sign` may sign. */
|
|
18
|
+
readonly signs: readonly string[];
|
|
19
|
+
/** The HTTPS origins the global `fetch` reaches while the site's handler runs. */
|
|
20
|
+
readonly egress: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/** A public procedure's reach: the texts the verified signed tokens of its request carry, which name the rows it changes. */
|
|
23
|
+
export interface PublicReach {
|
|
24
|
+
readonly carried: ReadonlySet<string>;
|
|
25
|
+
}
|
|
26
|
+
export type SiteFunction = (args: {
|
|
27
|
+
readonly input?: unknown;
|
|
28
|
+
readonly ctx: object;
|
|
29
|
+
}) => unknown;
|
|
30
|
+
/**
|
|
31
|
+
* A handler tree leaf of a staged-kit site, which `serve` builds from each handled endpoint, job and webhook: the
|
|
32
|
+
* handler and the site's `declared` object. `boot` builds the site's `ctx.db` (and the declared handles) from it.
|
|
33
|
+
*/
|
|
34
|
+
export interface SiteHandler {
|
|
35
|
+
readonly handler: SiteFunction;
|
|
36
|
+
readonly declared: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** The leaf `serve` puts in the handler tree for one site. */
|
|
39
|
+
export declare function siteHandler(handler: SiteFunction, declared: unknown): SiteHandler;
|
|
40
|
+
/** Whether a handler tree node is a site leaf, rather than a group of handlers that happens to have the same fields. */
|
|
41
|
+
export declare function isSiteHandler(node: unknown): node is SiteHandler;
|
|
42
|
+
/**
|
|
43
|
+
* The site's `declared` object, parsed at the boundary: the kit's types already refused what is wrong in it, and this
|
|
44
|
+
* refuses it again, because a widened or cast declaration would otherwise reach further than its site shows.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseDeclaration(where: string, declared: unknown, kind: DataKind, artifact: ContractArtifact, problems: string[]): SiteDeclaration;
|
|
47
|
+
/**
|
|
48
|
+
* The declaration a site runs with: the one the approved contract holds (`ContractArtifact.declared`, which the
|
|
49
|
+
* contract's digest covers), by the site's endpoint id or `job:<name>`. A leaf whose own `declared` says otherwise is
|
|
50
|
+
* refused, so a declaration only the handler tree carries never widens a site past what was approved.
|
|
51
|
+
*/
|
|
52
|
+
export declare function approvedDeclaration(where: string, site: string, leaf: unknown, kind: DataKind, artifact: ContractArtifact, problems: string[]): SiteDeclaration;
|
|
53
|
+
/** What the approved artifact declares for `site` (an endpoint id or `job:<name>`); nothing when it holds none. */
|
|
54
|
+
export declare function approvedReach(site: string, artifact: ContractArtifact): SiteDeclaration;
|
|
55
|
+
/**
|
|
56
|
+
* Refuses a handle used outside the work it was made for: another request, another `boot`, an ended one. `handle` names
|
|
57
|
+
* it in the refusal: a `ctx.db` handle, or `ctx.sql`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function boundTo(frame: RequestFrame, ended: () => boolean, handle?: string): () => void;
|
|
60
|
+
/** One invocation's transaction, and the data handles of its site. */
|
|
61
|
+
export declare class Invocation {
|
|
62
|
+
#private;
|
|
63
|
+
constructor(frame: RequestFrame, data: DataAccess, readOnly: boolean);
|
|
64
|
+
/**
|
|
65
|
+
* The handles of the site, all of them running in this invocation's transaction. A public procedure's (`reach` its
|
|
66
|
+
* carried texts) insert and read under its actions only, and update or remove only a row a verified token names.
|
|
67
|
+
*/
|
|
68
|
+
handles(kind: DataKind, declaration: SiteDeclaration, principal: string, reach?: PublicReach | 'declared', files?: FileBinding): DataHandles;
|
|
69
|
+
/** `ctx.sql` of a site that declares it: its statements run in this invocation's transaction, like every handle's. */
|
|
70
|
+
sql(mode: SqlMode): SqlTag;
|
|
71
|
+
/** The check of `ctx.files`: bound to this invocation's transaction, like every handle. */
|
|
72
|
+
fileCheck(): () => void;
|
|
73
|
+
/** Where the framework writes for this site (a webhook binding): this invocation's transaction, bound like every handle. */
|
|
74
|
+
frameworkSite(handle: string): {
|
|
75
|
+
readonly check: () => void;
|
|
76
|
+
readonly session: () => Promise<Session>;
|
|
77
|
+
};
|
|
78
|
+
/** Commits; a request the caller abandoned meanwhile rolls back instead. */
|
|
79
|
+
commit(signal: AbortSignal | null): Promise<void>;
|
|
80
|
+
rollback(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
/** What a request's access form handed the handler, by `ctx` name. */
|
|
83
|
+
export interface AccessCapabilities {
|
|
84
|
+
readonly target?: unknown;
|
|
85
|
+
readonly within?: unknown;
|
|
86
|
+
readonly created?: unknown;
|
|
87
|
+
/** An `app` endpoint's capability, `ctx.app`. */
|
|
88
|
+
readonly app?: unknown;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `ctx.db` of an endpoint, by its access form: the API matrix of `docs/reference/application-runtime.md`. An endpoint of
|
|
92
|
+
* several capabilities binds each, as a job binds what it minted: `ctx.db.on(ref)`, `.in(ref)`, `.over(within)`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function endpointData(handles: DataHandles, form: 'on' | 'in' | 'over' | 'creates' | 'app' | 'member' | 'capabilities', access: AccessCapabilities): object;
|
|
95
|
+
/** `ctx.db` of a job or a webhook: each capability it minted or received binds the same handles an endpoint gets. */
|
|
96
|
+
export declare function siteData(handles: DataHandles, app: (action: string) => object): object;
|
|
97
|
+
/**
|
|
98
|
+
* `ctx.app` and `ctx.db.app` of a webhook bound to a dimension. The types already refuse them; at run time they exist
|
|
99
|
+
* only to refuse by name.
|
|
100
|
+
*/
|
|
101
|
+
export declare function boundWebhookApp(webhook: string): (action: string) => never;
|
|
102
|
+
/**
|
|
103
|
+
* The handles of a job attempt, outside its fenced transaction (`'job'`, each read on its own) or inside it. `tickMs`,
|
|
104
|
+
* the job's schedule period, is a queue claim's first backoff.
|
|
105
|
+
*/
|
|
106
|
+
export declare function jobHandles(frame: RequestFrame, data: DataAccess, declaration: SiteDeclaration, principal: string, kind: 'job' | 'mutation', session: () => Promise<Session>, ended: () => boolean, tickMs?: number, files?: FileBinding): DataHandles;
|
|
107
|
+
/** `ctx.sql` (or `member`, a job's `tx.sql`) of a site that does not declare `sql`. */
|
|
108
|
+
export declare function undeclaredSql(site: string, kind: DataKind, member?: 'ctx.sql' | 'tx.sql'): SqlTag;
|
|
109
|
+
/** `ctx.sql` of a job that declares `sql`, outside `ctx.transaction`: its statements run only in the attempt's fence. */
|
|
110
|
+
export declare function jobSqlOutsideTransaction(job: string): SqlTag;
|
|
111
|
+
/** `tx.sql` of a job site that declares it: its statements run in the attempt's fenced transaction. */
|
|
112
|
+
export declare function jobSql(frame: RequestFrame, data: DataAccess, mode: SqlMode, session: Session, ended: () => boolean): SqlTag;
|
|
113
|
+
/**
|
|
114
|
+
* `ctx.kick` of one invocation: it names only a declared job, and records it; the dispatcher sends each recorded job once,
|
|
115
|
+
* after the commit, and nothing on a rollback.
|
|
116
|
+
*/
|
|
117
|
+
export declare function kickRecorder(site: string, declared: readonly string[]): {
|
|
118
|
+
readonly kick: (job: string) => void;
|
|
119
|
+
readonly kicked: () => readonly string[];
|
|
120
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { IamMembership, MembershipRequestCaller } from '@fabrika/auth';
|
|
2
|
+
import type { EndpointDirectory, InvitationItem, InvitationOptions, MemberItem, PrincipalAffiliation, Scope } from '@fabrika/auth-core';
|
|
3
|
+
import type { CapabilityAuthority } from '@fabrika/db/capability';
|
|
4
|
+
import { type Ref } from './capability';
|
|
5
|
+
/** Who a membership is about: an address IAM invites, or a principal it already has. */
|
|
6
|
+
export type MemberTarget = {
|
|
7
|
+
readonly email: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly principalId: string;
|
|
10
|
+
};
|
|
11
|
+
export type AssignedMember = {
|
|
12
|
+
readonly assigned: {
|
|
13
|
+
readonly principalId: string;
|
|
14
|
+
readonly role: string;
|
|
15
|
+
};
|
|
16
|
+
} | {
|
|
17
|
+
readonly invited: {
|
|
18
|
+
readonly invitationId: string;
|
|
19
|
+
readonly role: string;
|
|
20
|
+
readonly expiresAt: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export interface MemberList {
|
|
24
|
+
readonly members: readonly MemberItem[];
|
|
25
|
+
readonly invitations: readonly InvitationItem[];
|
|
26
|
+
}
|
|
27
|
+
/** `ctx.members` of an endpoint whose action is its dimension's `members.manage`: one write per request. */
|
|
28
|
+
export interface MemberManagement<D extends string, A extends string, R extends string> {
|
|
29
|
+
/** The members of the value; IAM requires the dimension's `members.list` there. */
|
|
30
|
+
list(ref: Ref<D, A>): Promise<MemberList>;
|
|
31
|
+
/**
|
|
32
|
+
* Gives `role` in the value: an existing principal gets the grant, an address an invitation. Repeating it answers the
|
|
33
|
+
* same and sends nothing. For an address, `options` names the company (`label`, at most 200 characters on one line)
|
|
34
|
+
* and a personal `message` (at most 1000 characters of plain text) the e-mail carries, and `resend` gives an open
|
|
35
|
+
* invitation a new link and a new 7 days and sends it again; the old link stops working.
|
|
36
|
+
*/
|
|
37
|
+
assign(ref: Ref<D, A>, member: MemberTarget, role: R, options?: InvitationOptions): Promise<AssignedMember>;
|
|
38
|
+
/** Removes the member's roles there (only `role` when given), or cancels an invitation. Repeating it answers the same. */
|
|
39
|
+
revoke(ref: Ref<D, A>, member: {
|
|
40
|
+
readonly principalId: string;
|
|
41
|
+
readonly role?: R;
|
|
42
|
+
} | {
|
|
43
|
+
readonly invitationId: string;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** One member of the target, as a member directory lists it. */
|
|
47
|
+
export interface DirectoryMember<L extends string | null = string | null> {
|
|
48
|
+
readonly principalId: string;
|
|
49
|
+
/**
|
|
50
|
+
* The principal's display label (a user's spelling of its address): `string` where the directory's dimension declares
|
|
51
|
+
* `directory: 'labels'`, since IAM then answers it to every caller; elsewhere null unless the caller manages its members.
|
|
52
|
+
*/
|
|
53
|
+
readonly label: L;
|
|
54
|
+
/** Its membership roles in the target. */
|
|
55
|
+
readonly roles: readonly string[];
|
|
56
|
+
readonly disabled: boolean;
|
|
57
|
+
/** `external`: a person from outside the organization (ADR-0051), for a staff UI to show. */
|
|
58
|
+
readonly affiliation: PrincipalAffiliation;
|
|
59
|
+
}
|
|
60
|
+
/** One member a lookup found: never its email, and its label only where the directory shows labels. */
|
|
61
|
+
export interface Member<L extends string | null = string | null> {
|
|
62
|
+
readonly principalId: string;
|
|
63
|
+
/** The principal's display label, as `DirectoryMember.label`. */
|
|
64
|
+
readonly label: L;
|
|
65
|
+
readonly disabled: boolean;
|
|
66
|
+
readonly affiliation: PrincipalAffiliation;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `ctx.members` of an endpoint on or in a dimension below a member directory (its own dimension's, or the nearest one
|
|
70
|
+
* containing it): the members of the value its object lies in. IAM checks the endpoint's own action on that value.
|
|
71
|
+
*/
|
|
72
|
+
export interface MemberLookup<L extends string | null = string | null> {
|
|
73
|
+
/** The principal, when it is a member of the directory's value; null otherwise, an unknown id included. */
|
|
74
|
+
get(principalId: string): Promise<Member<L> | null>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `ctx.members` of an endpoint whose action is the directory's `members.list`: also every member, read from IAM, which
|
|
78
|
+
* checks that action live on the directory's value. No emails and no invitations: those stay with `members.manage`.
|
|
79
|
+
*/
|
|
80
|
+
export interface MemberDirectory<L extends string | null = string | null> extends MemberLookup<L> {
|
|
81
|
+
list(): Promise<readonly DirectoryMember<L>[]>;
|
|
82
|
+
}
|
|
83
|
+
/** One role a person holds at the application: globally (`scope: null`), or in one value. */
|
|
84
|
+
export interface ApplicationMemberRole {
|
|
85
|
+
readonly role: string;
|
|
86
|
+
readonly scope: Scope | null;
|
|
87
|
+
}
|
|
88
|
+
/** One of the application's own people, as its directory lists them. */
|
|
89
|
+
export interface ApplicationMember {
|
|
90
|
+
readonly principalId: string;
|
|
91
|
+
/** The principal's display label; null unless the vocabulary declares `directory: 'labels'` or the caller manages the people. */
|
|
92
|
+
readonly label: string | null;
|
|
93
|
+
/** The person's address; null unless the caller holds the application's `members.manage` globally. */
|
|
94
|
+
readonly email: string | null;
|
|
95
|
+
/** Unix seconds of the person's last sign-in to the application; null for one who never did, and as `email`. */
|
|
96
|
+
readonly lastSignInAt: number | null;
|
|
97
|
+
/** Every role the person holds at the application, a global one first. */
|
|
98
|
+
readonly roles: readonly ApplicationMemberRole[];
|
|
99
|
+
readonly disabled: boolean;
|
|
100
|
+
readonly affiliation: PrincipalAffiliation;
|
|
101
|
+
}
|
|
102
|
+
export interface ApplicationMemberPage {
|
|
103
|
+
readonly members: readonly ApplicationMember[];
|
|
104
|
+
/** The `after` of the following page; null on the last one. */
|
|
105
|
+
readonly next: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `ctx.members` of an `app: true` endpoint whose action is the application's `members.list`: every principal holding a
|
|
109
|
+
* role of the application, globally or in a value, a page of at most 100 at a time. IAM checks the action live.
|
|
110
|
+
*/
|
|
111
|
+
export interface ApplicationDirectory {
|
|
112
|
+
list(options?: {
|
|
113
|
+
readonly after?: string | undefined;
|
|
114
|
+
}): Promise<ApplicationMemberPage>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The application's own directory, read from IAM with the caller's token. A runtime without an IAM membership transport
|
|
118
|
+
* still boots, and only a call fails.
|
|
119
|
+
*/
|
|
120
|
+
export declare function applicationDirectory(iam: IamMembership | null, caller: MembershipRequestCaller, endpoint: string): ApplicationDirectory;
|
|
121
|
+
export interface IssuedLink {
|
|
122
|
+
readonly id: string;
|
|
123
|
+
/** `<prefix>/#<key>`: the key travels in the fragment, which never reaches a server or a `Referer`. */
|
|
124
|
+
readonly url: string;
|
|
125
|
+
readonly expiresAt: number;
|
|
126
|
+
}
|
|
127
|
+
/** `ctx.links` of a site that declares the link mount it issues. */
|
|
128
|
+
export interface LinkIssuing<D extends string, A extends string, M extends string> {
|
|
129
|
+
issue(ref: Ref<D, A>, options: {
|
|
130
|
+
readonly mount: M;
|
|
131
|
+
readonly days: number;
|
|
132
|
+
}): Promise<IssuedLink>;
|
|
133
|
+
/**
|
|
134
|
+
* Ends the link `id` (`IssuedLink.id`) at IAM: its issuer or a holder of `members.manage` on its chain may; a job only
|
|
135
|
+
* a link it issued. Repeating it answers the same. A token minted from it still works until it expires, at most 300 s.
|
|
136
|
+
*/
|
|
137
|
+
revoke(id: string): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
/** One request's authority at IAM: the caller's token, the proxy's request id and the delegation grant it obtained. */
|
|
140
|
+
export interface MembershipRequest {
|
|
141
|
+
readonly iam: IamMembership;
|
|
142
|
+
readonly caller: MembershipRequestCaller;
|
|
143
|
+
readonly delegation: string | null;
|
|
144
|
+
/** The endpoint's own access: the dimension a `Ref` passed to IAM must name, and its action. */
|
|
145
|
+
readonly dimension: string;
|
|
146
|
+
readonly action: string;
|
|
147
|
+
/** The runtime's authority, which the `Ref` passed to IAM must resolve with. */
|
|
148
|
+
readonly capabilities: CapabilityAuthority;
|
|
149
|
+
}
|
|
150
|
+
/** `creates`: IAM generates the value and grants the caller the dimension's creator role, atomically. */
|
|
151
|
+
export declare function createScope(request: MembershipRequest): Promise<string>;
|
|
152
|
+
export declare function memberManagement(request: MembershipRequest): MemberManagement<string, string, string>;
|
|
153
|
+
/** The directory an endpoint reads (`endpointDirectory`), and whether its dimension declares `directory: 'labels'`. */
|
|
154
|
+
export interface ReadDirectory extends EndpointDirectory {
|
|
155
|
+
readonly labels: boolean;
|
|
156
|
+
}
|
|
157
|
+
/** The directory an endpoint reads, with the endpoint a fault names. */
|
|
158
|
+
export interface DirectoryReach extends ReadDirectory {
|
|
159
|
+
readonly endpoint: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The member directory of the request's own target, `target`: its dimension's when it declares members, else the
|
|
163
|
+
* nearest container's, at the value the target's chain holds. A runtime without an IAM membership transport still
|
|
164
|
+
* boots, and only a call fails.
|
|
165
|
+
*/
|
|
166
|
+
export declare function memberDirectory(request: MembershipRequest | null, target: unknown, reach: DirectoryReach): MemberLookup | MemberDirectory;
|
|
167
|
+
/** A link mount an endpoint may issue for: its dimension and issuing action are the endpoint's. */
|
|
168
|
+
export interface IssuableLink {
|
|
169
|
+
readonly name: string;
|
|
170
|
+
readonly prefix: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `ctx.links`: IAM issues an anonymous key bound to the object's whole containment chain, read from the `Ref`, so a
|
|
174
|
+
* link whose object moves stops working. `containers` is the object's dimension's containers, nearest first.
|
|
175
|
+
*/
|
|
176
|
+
export declare function linkIssuing(request: MembershipRequest, mounts: readonly IssuableLink[], containers: readonly string[]): LinkIssuing<string, string, string>;
|
|
177
|
+
/** The link mount a cron job declares it `issues`, as the artifact states it. */
|
|
178
|
+
export interface JobLinkMount {
|
|
179
|
+
readonly name: string;
|
|
180
|
+
readonly prefix: string;
|
|
181
|
+
readonly dimension: string;
|
|
182
|
+
readonly issuedBy: string;
|
|
183
|
+
}
|
|
184
|
+
/** A job's authority at IAM: its link mount, and the token IAM signs from its key now. */
|
|
185
|
+
export interface JobLinkRequest {
|
|
186
|
+
readonly iam: IamMembership;
|
|
187
|
+
readonly job: string;
|
|
188
|
+
readonly mount: JobLinkMount;
|
|
189
|
+
/** The mount dimension's containers, nearest first. */
|
|
190
|
+
readonly containers: readonly string[];
|
|
191
|
+
/** The job's current token and this attempt's id; it throws a 503 when IAM signs none. */
|
|
192
|
+
caller(): Promise<MembershipRequestCaller>;
|
|
193
|
+
/** Throws once the attempt has ended. It runs synchronously right before each IAM operation, after every await. */
|
|
194
|
+
assertLive(): void;
|
|
195
|
+
readonly capabilities: CapabilityAuthority;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* `ctx.links` of a job that declares `issues`. IAM admits the job's token on `issueLink` and `revokeLink` only while the
|
|
199
|
+
* approved schema says the job issues this mount, so a job holds no more than its declaration; it revokes only its own.
|
|
200
|
+
*/
|
|
201
|
+
export declare function jobLinkIssuing(request: JobLinkRequest): LinkIssuing<string, string, string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A body that is not the `multipart/form-data` a form reads (`malformed`), or one whose stream failed before its end
|
|
3
|
+
* (`interrupted`: the client closed the connection). Its message names what is wrong, never a byte of it.
|
|
4
|
+
*/
|
|
5
|
+
export declare class MultipartError extends Error {
|
|
6
|
+
readonly name = "MultipartError";
|
|
7
|
+
readonly reason: 'malformed' | 'interrupted';
|
|
8
|
+
constructor(message: string, reason?: 'malformed' | 'interrupted');
|
|
9
|
+
}
|
|
10
|
+
export interface MultipartPart {
|
|
11
|
+
/** The `name` of its `Content-Disposition`. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** Its `filename`, as sent; `null` when the part has none, which is a plain field. */
|
|
14
|
+
readonly filename: string | null;
|
|
15
|
+
/** The media type of its `Content-Type`, lower-cased and without parameters; `null` when it has none. */
|
|
16
|
+
readonly contentType: string | null;
|
|
17
|
+
/** Its bytes. Read it to the end, or cancel it, before the next part. */
|
|
18
|
+
readonly body: ReadableStream<Uint8Array>;
|
|
19
|
+
}
|
|
20
|
+
export interface MultipartReader {
|
|
21
|
+
/** The next part, or `null` after the closing delimiter. Rejects while the previous part's body is unread. */
|
|
22
|
+
next(): Promise<MultipartPart | null>;
|
|
23
|
+
/** Stops reading the body. */
|
|
24
|
+
cancel(): Promise<void>;
|
|
25
|
+
/** Why the body broke the format, wherever the break was met (a part's body included); `null` while it did not. */
|
|
26
|
+
failure(): MultipartError | null;
|
|
27
|
+
}
|
|
28
|
+
/** The boundary of a `multipart/form-data` `Content-Type`, or `null` when it is another type or names none. */
|
|
29
|
+
export declare function multipartBoundary(contentType: string | null): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Reads `body` as `multipart/form-data` with `boundary`. A part's headers hold at most `maxHeaderBytes`; its body is
|
|
32
|
+
* as long as the sender makes it, so the caller bounds what it reads of each.
|
|
33
|
+
*/
|
|
34
|
+
export declare function multipartParts(body: ReadableStream<Uint8Array>, boundary: string, options: {
|
|
35
|
+
readonly maxHeaderBytes: number;
|
|
36
|
+
}): MultipartReader;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SchemaShape } from '@fabrika/auth-core';
|
|
2
|
+
/** The longest URL a procedure reads its parameters from. */
|
|
3
|
+
export declare const MAX_URL_LENGTH = 8192;
|
|
4
|
+
/** The parameters of `params` as the object `shape`'s fields read them; `shape` is a flat object, or `null` for none. */
|
|
5
|
+
export declare function paramsObject(params: URLSearchParams, shape: SchemaShape | null): Record<string, unknown>;
|
|
6
|
+
/** The URL's parameters of a request, within `MAX_URL_LENGTH`. */
|
|
7
|
+
export declare function urlParams(request: Request): URLSearchParams;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type AccessTokenClaims, type Caller, type PermissionEntry } from '@fabrika/auth-core';
|
|
2
|
+
import type { PageCaller } from '../client/caller';
|
|
3
|
+
/** A person: the IAM session, or a `px_` key bound to a user principal. */
|
|
4
|
+
export interface UserPrincipal {
|
|
5
|
+
readonly kind: 'user';
|
|
6
|
+
readonly id: string;
|
|
7
|
+
/** What IAM shows for the person: the address as it was spelled. Display it; never mail it. */
|
|
8
|
+
readonly label: string;
|
|
9
|
+
/** The mailbox IAM recorded for the person, lower-cased: the one to mail, not proof of possession. Only a person has one. */
|
|
10
|
+
readonly email: string;
|
|
11
|
+
}
|
|
12
|
+
/** A machine: a `px_` key bound to a service principal. */
|
|
13
|
+
export interface ServicePrincipal {
|
|
14
|
+
readonly kind: 'service';
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly label: string;
|
|
17
|
+
}
|
|
18
|
+
/** The anonymous holder of a share link; the id is the link's credential, not a person. */
|
|
19
|
+
export interface LinkHolder {
|
|
20
|
+
readonly kind: 'link';
|
|
21
|
+
readonly credentialId: string;
|
|
22
|
+
}
|
|
23
|
+
/** Another application, through its connector key. */
|
|
24
|
+
export interface AppPrincipal<Id extends string = string> {
|
|
25
|
+
readonly kind: 'app';
|
|
26
|
+
readonly app: Id;
|
|
27
|
+
readonly id: string;
|
|
28
|
+
}
|
|
29
|
+
export type Principal = UserPrincipal | ServicePrincipal | LinkHolder | AppPrincipal;
|
|
30
|
+
/** Whoever called a public procedure: nobody the platform knows. The handler runs as the endpoint's own workload. */
|
|
31
|
+
export interface PublicPrincipal {
|
|
32
|
+
readonly kind: 'public';
|
|
33
|
+
/** The endpoint id, which is the workload's name: `<mount>.<group>.<procedure>`. */
|
|
34
|
+
readonly endpoint: string;
|
|
35
|
+
}
|
|
36
|
+
export type PrincipalOf<C> = C extends 'human' ? UserPrincipal : C extends 'machine' ? ServicePrincipal : C extends 'link' ? LinkHolder : C extends `app:${infer Id}` ? AppPrincipal<Id> : never;
|
|
37
|
+
export interface Presented {
|
|
38
|
+
readonly caller: Caller;
|
|
39
|
+
readonly principal: Principal;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Why a verified token presents no principal: it is no caller of an application (`no_caller`), or a person's token
|
|
43
|
+
* from an IAM that does not mint the mailbox (`no_mailbox`), which a handler could not read.
|
|
44
|
+
*/
|
|
45
|
+
export type Unpresented = 'no_caller' | 'no_mailbox';
|
|
46
|
+
/** The caller a verified application token presents, by `callerOf` (the proxy reads the same), and its principal. */
|
|
47
|
+
export declare function presentedBy(claims: AccessTokenClaims): Presented | Unpresented;
|
|
48
|
+
/**
|
|
49
|
+
* The caller document of an SPA mount: the person and the contract's actions each of the token's entries covers, a
|
|
50
|
+
* pattern listed as those actions, global entries apart from each scope value's. Built from the token alone.
|
|
51
|
+
*/
|
|
52
|
+
export declare function pageCallerOf(principal: UserPrincipal, perms: readonly PermissionEntry[], actions: readonly string[]): PageCaller;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
/** No branch of a union accepts the value, whatever it is reduced to. */
|
|
3
|
+
export declare class ProjectionRefusedError extends Error {
|
|
4
|
+
readonly name = "ProjectionRefusedError";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `value` with every object reduced to the fields `schema` declares, recursively. A union takes the first branch,
|
|
8
|
+
* in declaration order, whose schema accepts the value reduced to that branch. Anything else is left for
|
|
9
|
+
* validation.
|
|
10
|
+
*/
|
|
11
|
+
export declare function project(schema: z.core.$ZodType, value: unknown): unknown;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type AdmissionRate, type ContractArtifact } from '@fabrika/auth-core';
|
|
2
|
+
import { type ChallengeFetch, type ChallengeIssuer, type ChallengeVerifier, type ChallengeWork } from './challenge';
|
|
3
|
+
import type { RequestClient } from './types';
|
|
4
|
+
/** The bucket every call of an endpoint takes from; a client's is `client:<digest>`, a caller's `caller:<digest>`. */
|
|
5
|
+
export declare const ENDPOINT_BUCKET = "endpoint";
|
|
6
|
+
/** The longest `User-Agent` a handler reads: enough to tell browsers apart, too short to carry a payload. */
|
|
7
|
+
export declare const MAX_USER_AGENT_LENGTH = 256;
|
|
8
|
+
/** A procedure's admission and time limit, from the approved artifact. */
|
|
9
|
+
export interface ProcedureAdmission {
|
|
10
|
+
readonly rate: AdmissionRate;
|
|
11
|
+
/** The challenge's verifier, with the secret its application variable holds; `null` without a challenge. */
|
|
12
|
+
readonly challenge: AdmittedChallenge | null;
|
|
13
|
+
/** The one token the challenge does not record, so it passes again: `fabrika app dev`'s test widget's. */
|
|
14
|
+
readonly repeatableToken: string | null;
|
|
15
|
+
/** How long the handler may run after the last body byte. */
|
|
16
|
+
readonly timeoutMs: number;
|
|
17
|
+
}
|
|
18
|
+
/** A challenge's verifier and the widget action the verification expects back; an ALTCHA one's issuer too. */
|
|
19
|
+
export interface AdmittedChallenge {
|
|
20
|
+
readonly verifier: ChallengeVerifier;
|
|
21
|
+
readonly action: string;
|
|
22
|
+
/** What the procedure's issuing endpoint answers with; `null` for a provider that issues its own (Turnstile). */
|
|
23
|
+
readonly issuer: ChallengeIssuer | null;
|
|
24
|
+
}
|
|
25
|
+
/** A procedure the artifact holds no rules for (a `defineContract` one): no bucket, the request budget. */
|
|
26
|
+
export declare const DEFAULT_ADMISSION: ProcedureAdmission;
|
|
27
|
+
export interface AdmissionOptions {
|
|
28
|
+
readonly application: Readonly<Record<string, unknown>>;
|
|
29
|
+
readonly challengeFetch?: ChallengeFetch;
|
|
30
|
+
readonly repeatableChallengeToken?: string;
|
|
31
|
+
/** The work of every ALTCHA challenge in place of the contract's: a test kit's and `fabrika app dev`'s. */
|
|
32
|
+
readonly challengeWork?: ChallengeWork;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The admission of endpoint `id`, or `null` after a problem: a challenge whose secret's application variable is not set,
|
|
36
|
+
* or an ALTCHA key too short to resist a guess, keeps the application from booting, as a form's does.
|
|
37
|
+
*/
|
|
38
|
+
export declare function procedureAdmission(id: string, artifact: ContractArtifact, options: AdmissionOptions, problems: string[]): ProcedureAdmission | null;
|
|
39
|
+
/** The buckets a request takes from, in order: its client's or its caller's first, then the endpoint's. */
|
|
40
|
+
export declare function bucketsOf(rate: AdmissionRate, request: Request, caller: string | null): {
|
|
41
|
+
readonly bucket: string;
|
|
42
|
+
readonly limit: number;
|
|
43
|
+
}[];
|
|
44
|
+
/**
|
|
45
|
+
* Who sent the request, as the proxy saw it. The address is `X-Fabrika-Client-Ip`, which only the proxy sets, and only
|
|
46
|
+
* where the runtime adapter vouches that the proxy saw the visitor (`vouched`); behind a balancer whose range the
|
|
47
|
+
* platform does not trust it is `null`. Anything that is no IP address is `null` too.
|
|
48
|
+
*/
|
|
49
|
+
export declare function requestClient(request: Request, vouched: boolean): RequestClient;
|
|
50
|
+
/**
|
|
51
|
+
* At most `limit` requests at once, beyond which the next is refused: the share of the connection pool all public
|
|
52
|
+
* procedures of an instance hold together, so a flood of anonymous calls leaves signed-in staff the rest.
|
|
53
|
+
*/
|
|
54
|
+
export declare class InFlightShare {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(limit: number);
|
|
57
|
+
/** A release to call once the request is answered, or `null` when the share is full. */
|
|
58
|
+
enter(): (() => void) | null;
|
|
59
|
+
}
|
|
60
|
+
/** A quarter of the pool, at least one connection. */
|
|
61
|
+
export declare function publicShareOf(connections: number): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EmailSender } from '@fabrika/email';
|
|
2
|
+
import type { AnyContract } from '../contract/define';
|
|
3
|
+
import { type ApplicationEnvironment, type ContractImplementation, type DepsRuntime, type ImplementationDatabase } from './implement';
|
|
4
|
+
/**
|
|
5
|
+
* The application's dependencies, built at each `boot` from its own environment. A handler imports the module that
|
|
6
|
+
* defines them and reads `get()` while it runs: it answers what the boot serving that invocation built. At module load
|
|
7
|
+
* there is nothing to read yet.
|
|
8
|
+
*/
|
|
9
|
+
export interface AppDeps<D> {
|
|
10
|
+
get(): D;
|
|
11
|
+
}
|
|
12
|
+
/** Declares how each `boot` builds the dependencies; `serve(contract, { deps })` takes the result. */
|
|
13
|
+
export declare function defineDeps<D>(build: (env: ApplicationEnvironment, runtime: DepsRuntime) => D): AppDeps<D>;
|
|
14
|
+
export interface ServeOptions {
|
|
15
|
+
/** `scoped(tables)` of `@fabrika/db`, which `boot` binds, or `null` for a kit without tables (`defineApp(vocabulary)`). */
|
|
16
|
+
readonly database: ImplementationDatabase | null;
|
|
17
|
+
readonly deps?: AppDeps<unknown>;
|
|
18
|
+
/** The transport of the jobs that declare `mailer: 'own'`, built once at boot from the application's own variables. */
|
|
19
|
+
readonly mail?: (env: ApplicationEnvironment) => EmailSender | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The contract with the handlers its endpoints carry, ready for `boot`. The default export of `src/app.ts`. Throws
|
|
23
|
+
* `ContractError` naming every endpoint, job and webhook that took no `.handle(…)`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serve<const C extends AnyContract>(contract: C, options: ServeOptions): ContractImplementation<C>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ContractArtifact, type SchemaShape } from '@fabrika/auth-core';
|
|
2
|
+
import type { Schema } from '../contract/define';
|
|
3
|
+
/** One key of the environment: its id, which each token it signs carries, and its secret. */
|
|
4
|
+
export interface TokenKey {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly secret: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
/** A purpose as the runtime holds it: the claims schema the application declared, and the longest lifetime. */
|
|
9
|
+
export interface RuntimePurpose {
|
|
10
|
+
readonly claims: Schema;
|
|
11
|
+
readonly maxDays: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The keys of `FABRIKA_TOKEN_KEYS`: `<kid>:<secret>` entries joined by `,`, the signing key first. Required when the
|
|
15
|
+
* contract declares a token purpose; refused at boot when malformed. `null` without purposes or after a problem.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readTokenKeys(framework: Readonly<Record<string, string>>, artifact: ContractArtifact, problems: string[]): readonly TokenKey[] | null;
|
|
18
|
+
/** The claims a verified token carried, and every text in them: a redirect target or a row id the request may use. */
|
|
19
|
+
export interface VerifiedTokens {
|
|
20
|
+
readonly carried: ReadonlySet<string>;
|
|
21
|
+
}
|
|
22
|
+
export declare const NO_VERIFIED_TOKENS: VerifiedTokens;
|
|
23
|
+
/**
|
|
24
|
+
* The environment's signed tokens. `purposes` are the application's declared claims schemas, by purpose; `keys` are
|
|
25
|
+
* `null` when the contract declares none, so a sign or a verification refuses.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SignedTokens {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(purposes: ReadonlyMap<string, RuntimePurpose>, keys: readonly TokenKey[] | null, now?: () => number);
|
|
30
|
+
/**
|
|
31
|
+
* `ctx.tokens` of a site: signs a token of a purpose `signs` names, valid for `days` (at most the purpose's
|
|
32
|
+
* `maxDays`). The claims are parsed by the purpose's schema first, so a token never carries what a verifier refuses.
|
|
33
|
+
*/
|
|
34
|
+
context(site: string, signs: readonly string[]): {
|
|
35
|
+
readonly sign: (purpose: string, claims: unknown, options: unknown) => string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The claims of `token` when it is a token of `purpose` this environment signed and it has not expired; a 400
|
|
39
|
+
* otherwise, which says only that it is not valid, so a caller learns nothing about the keys.
|
|
40
|
+
*/
|
|
41
|
+
verify(purpose: string, token: string): Readonly<Record<string, unknown>>;
|
|
42
|
+
/**
|
|
43
|
+
* Each top-level signed token field of `value` (by `shape`, the approved input or query) replaced by its verified
|
|
44
|
+
* claims, and every text those claims carry.
|
|
45
|
+
*/
|
|
46
|
+
verifyFields(shape: SchemaShape | null, value: Readonly<Record<string, unknown>>, into: Set<string>): Readonly<Record<string, unknown>>;
|
|
47
|
+
}
|
|
48
|
+
/** The top-level fields of an object shape that carry a signed token, optional or not, with their purposes. */
|
|
49
|
+
export declare function tokenFields(shape: SchemaShape | null): ReadonlyMap<string, string>;
|