@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.
Files changed (134) hide show
  1. package/package.json +27 -26
  2. package/skill/SKILL.md +1 -1
  3. package/skill/references/app-setup.md +8 -8
  4. package/src/dev/changes.ts +5 -43
  5. package/src/dev/contract-digest.ts +17 -0
  6. package/src/dev/index.ts +1 -1
  7. package/src/dev/launcher.ts +69 -22
  8. package/src/dev/wire.ts +2 -1
  9. package/types/src/agent-skill.d.ts +29 -0
  10. package/types/src/build.d.ts +11 -0
  11. package/types/src/bun.d.ts +66 -0
  12. package/types/src/client/caller.d.ts +19 -0
  13. package/types/src/client/contract.d.ts +131 -0
  14. package/types/src/client/error-reporting.d.ts +66 -0
  15. package/types/src/client/index.d.ts +4 -0
  16. package/types/src/client/rpc.d.ts +14 -0
  17. package/types/src/client/share.d.ts +2 -0
  18. package/types/src/client/transport.d.ts +42 -0
  19. package/types/src/cloudflare.d.ts +49 -0
  20. package/types/src/contract/artifact-schema.d.ts +8 -0
  21. package/types/src/contract/attachment.d.ts +41 -0
  22. package/types/src/contract/build.d.ts +12 -0
  23. package/types/src/contract/compile.d.ts +11 -0
  24. package/types/src/contract/connect.d.ts +51 -0
  25. package/types/src/contract/context.d.ts +501 -0
  26. package/types/src/contract/declaration.d.ts +303 -0
  27. package/types/src/contract/defaults.d.ts +208 -0
  28. package/types/src/contract/define.d.ts +359 -0
  29. package/types/src/contract/file.d.ts +44 -0
  30. package/types/src/contract/group.d.ts +274 -0
  31. package/types/src/contract/handled.d.ts +18 -0
  32. package/types/src/contract/index.d.ts +19 -0
  33. package/types/src/contract/kit.d.ts +210 -0
  34. package/types/src/contract/platform.d.ts +9 -0
  35. package/types/src/contract/problems.d.ts +14 -0
  36. package/types/src/contract/redirect.d.ts +25 -0
  37. package/types/src/contract/schema-from-shape.d.ts +10 -0
  38. package/types/src/contract/schema-shape.d.ts +5 -0
  39. package/types/src/contract/snapshot-source.d.ts +15 -0
  40. package/types/src/contract/token.d.ts +58 -0
  41. package/types/src/contract/vocabulary.d.ts +30 -0
  42. package/types/src/contract/webhook-binding.d.ts +24 -0
  43. package/types/src/contract/webhook-scheme.d.ts +23 -0
  44. package/types/src/dev/changes.d.ts +7 -0
  45. package/types/src/dev/contract-digest.d.ts +1 -0
  46. package/types/src/dev/index.d.ts +2 -0
  47. package/types/src/dev/launcher.d.ts +32 -0
  48. package/types/src/dev/output.d.ts +22 -0
  49. package/types/src/dev/wire.d.ts +122 -0
  50. package/types/src/disk-assets.d.ts +3 -0
  51. package/types/src/errors.d.ts +46 -0
  52. package/types/src/fetch.d.ts +7 -0
  53. package/types/src/files/archive.d.ts +26 -0
  54. package/types/src/files/clamscan.d.ts +32 -0
  55. package/types/src/files/handle.d.ts +106 -0
  56. package/types/src/files/kinds.d.ts +105 -0
  57. package/types/src/files/scan.d.ts +80 -0
  58. package/types/src/files/sniff.d.ts +7 -0
  59. package/types/src/files/streams.d.ts +17 -0
  60. package/types/src/files/treatment.d.ts +18 -0
  61. package/types/src/from-row.d.ts +20 -0
  62. package/types/src/honeypot.d.ts +2 -0
  63. package/types/src/implement/anonymous.d.ts +26 -0
  64. package/types/src/implement/attachment.d.ts +14 -0
  65. package/types/src/implement/boot.d.ts +109 -0
  66. package/types/src/implement/bounded.d.ts +22 -0
  67. package/types/src/implement/browser-config.d.ts +19 -0
  68. package/types/src/implement/capability.d.ts +140 -0
  69. package/types/src/implement/challenge.d.ts +75 -0
  70. package/types/src/implement/client-address.d.ts +11 -0
  71. package/types/src/implement/connect.d.ts +67 -0
  72. package/types/src/implement/crashes.d.ts +31 -0
  73. package/types/src/implement/dispatch.d.ts +218 -0
  74. package/types/src/implement/egress.d.ts +14 -0
  75. package/types/src/implement/environment.d.ts +14 -0
  76. package/types/src/implement/failures.d.ts +34 -0
  77. package/types/src/implement/faults.d.ts +44 -0
  78. package/types/src/implement/headers.d.ts +7 -0
  79. package/types/src/implement/implement.d.ts +58 -0
  80. package/types/src/implement/index.d.ts +24 -0
  81. package/types/src/implement/invocation.d.ts +120 -0
  82. package/types/src/implement/membership.d.ts +201 -0
  83. package/types/src/implement/multipart.d.ts +36 -0
  84. package/types/src/implement/params.d.ts +7 -0
  85. package/types/src/implement/principal.d.ts +52 -0
  86. package/types/src/implement/project.d.ts +11 -0
  87. package/types/src/implement/public.d.ts +61 -0
  88. package/types/src/implement/serve.d.ts +25 -0
  89. package/types/src/implement/tokens.d.ts +49 -0
  90. package/types/src/implement/types.d.ts +243 -0
  91. package/types/src/implement/urls.d.ts +15 -0
  92. package/types/src/implement/verifiers.d.ts +19 -0
  93. package/types/src/implement/webhook.d.ts +55 -0
  94. package/types/src/implement/workload.d.ts +56 -0
  95. package/types/src/implement/xlsx.d.ts +6 -0
  96. package/types/src/index.d.ts +12 -0
  97. package/types/src/jobs/exception.d.ts +40 -0
  98. package/types/src/jobs/mail.d.ts +67 -0
  99. package/types/src/jobs/report.d.ts +56 -0
  100. package/types/src/jobs/runner.d.ts +145 -0
  101. package/types/src/jobs/schedule.d.ts +15 -0
  102. package/types/src/jobs/scrub.d.ts +12 -0
  103. package/types/src/jobs/types.d.ts +92 -0
  104. package/types/src/react/caller.d.ts +30 -0
  105. package/types/src/react/errors.d.ts +40 -0
  106. package/types/src/react/hooks.d.ts +110 -0
  107. package/types/src/react/index.d.ts +5 -0
  108. package/types/src/react/provider.d.ts +58 -0
  109. package/types/src/react/store.d.ts +68 -0
  110. package/types/src/react/testing.d.ts +28 -0
  111. package/types/src/react/types.d.ts +33 -0
  112. package/types/src/rpc/builder.d.ts +52 -0
  113. package/types/src/rpc/contract.d.ts +23 -0
  114. package/types/src/rpc/dispatcher.d.ts +11 -0
  115. package/types/src/rpc/types.d.ts +62 -0
  116. package/types/src/service/define.d.ts +42 -0
  117. package/types/src/service/index.d.ts +3 -0
  118. package/types/src/service/serve.d.ts +28 -0
  119. package/types/src/standard-schema.d.ts +55 -0
  120. package/types/src/testing/caller.d.ts +95 -0
  121. package/types/src/testing/challenge.d.ts +11 -0
  122. package/types/src/testing/dev.d.ts +97 -0
  123. package/types/src/testing/endpoint-access.d.ts +99 -0
  124. package/types/src/testing/index.d.ts +12 -0
  125. package/types/src/testing/isolation.d.ts +3 -0
  126. package/types/src/testing/kit.d.ts +182 -0
  127. package/types/src/testing/membership.d.ts +132 -0
  128. package/types/src/testing/policy-isolation.d.ts +113 -0
  129. package/types/src/testing/preload.d.ts +1 -0
  130. package/types/src/testing/proxy.d.ts +40 -0
  131. package/types/src/testing/runner.d.ts +8 -0
  132. package/types/src/testing/sample-file.d.ts +5 -0
  133. package/types/src/testing/status.d.ts +4 -0
  134. package/types/src/undumpable.d.ts +5 -0
@@ -0,0 +1,52 @@
1
+ import type { AuthContext, Scope } from '@fabrika/auth';
2
+ import type { StandardSchemaV1 } from '../standard-schema.js';
3
+ import type { AuthRequirement, Procedure, Router, RouterDef } from './types.js';
4
+ type Handler<TContext, TInput, TOutput> = (args: {
5
+ ctx: TContext;
6
+ input: TInput;
7
+ }) => TOutput | Promise<TOutput>;
8
+ /** Builder before an output schema is set — the terminal infers the output type from the handler. */
9
+ export declare class ProcedureBuilder<TContext, TInput> {
10
+ private readonly _input;
11
+ private readonly _requirements;
12
+ constructor(_input: StandardSchemaV1<unknown, TInput>, _requirements: ReadonlyArray<AuthRequirement<TContext, TInput>>);
13
+ /** Set the input schema — ANY Standard Schema validator (zod, valibot, arktype, …). Resets requirements. */
14
+ input<TNewInput>(schema: StandardSchemaV1<unknown, TNewInput>): ProcedureBuilder<TContext, TNewInput>;
15
+ /** Set an output schema → switch to the output builder (handler result validated against it). */
16
+ output<TOutput>(schema: StandardSchemaV1<unknown, TOutput>): OutputProcedureBuilder<TContext, TInput, TOutput>;
17
+ /**
18
+ * Record an authz requirement, checked BEFORE the handler runs (all must pass).
19
+ * Only available when the context carries `auth: AuthContext`.
20
+ */
21
+ require<TThis extends {
22
+ auth: AuthContext;
23
+ }>(this: ProcedureBuilder<TThis, TInput>, action: string, scopeResolver?: (input: TInput, ctx: TThis) => Scope | undefined): ProcedureBuilder<TThis, TInput>;
24
+ /** Terminal: a neutral handler (tagged `query`). Output type inferred from the return. */
25
+ handler<TOutput>(fn: Handler<TContext, TInput, TOutput>): Procedure<TContext, TInput, TOutput>;
26
+ /** Terminal: a `query`-tagged handler. Output type inferred from the return. */
27
+ query<TOutput>(fn: Handler<TContext, TInput, TOutput>): Procedure<TContext, TInput, TOutput>;
28
+ /** Terminal: a `mutation`-tagged handler. Output type inferred from the return. */
29
+ mutation<TOutput>(fn: Handler<TContext, TInput, TOutput>): Procedure<TContext, TInput, TOutput>;
30
+ }
31
+ /** Builder after `.output(schema)` — the terminal fixes the output to the schema type + validates it. */
32
+ export declare class OutputProcedureBuilder<TContext, TInput, TOutput> {
33
+ private readonly _input;
34
+ private readonly _output;
35
+ private readonly _requirements;
36
+ constructor(_input: StandardSchemaV1<unknown, TInput>, _output: StandardSchemaV1<unknown, TOutput>, _requirements: ReadonlyArray<AuthRequirement<TContext, TInput>>);
37
+ /** Record an authz requirement, checked BEFORE the handler runs (all must pass). */
38
+ require<TThis extends {
39
+ auth: AuthContext;
40
+ }>(this: OutputProcedureBuilder<TThis, TInput, TOutput>, action: string, scopeResolver?: (input: TInput, ctx: TThis) => Scope | undefined): OutputProcedureBuilder<TThis, TInput, TOutput>;
41
+ /** Terminal: a neutral handler (tagged `query`); result validated against the output schema. */
42
+ handler(fn: Handler<TContext, TInput, TOutput>): Procedure<TContext, TInput, TOutput>;
43
+ /** Terminal: a `query`-tagged handler; result validated against the output schema. */
44
+ query(fn: Handler<TContext, TInput, TOutput>): Procedure<TContext, TInput, TOutput>;
45
+ /** Terminal: a `mutation`-tagged handler; result validated against the output schema. */
46
+ mutation(fn: Handler<TContext, TInput, TOutput>): Procedure<TContext, TInput, TOutput>;
47
+ }
48
+ export declare function initRpc<TContext>(): {
49
+ procedure: ProcedureBuilder<TContext, void>;
50
+ router<TDef extends RouterDef>(def: TDef): Router<TDef>;
51
+ };
52
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { InferRouterClient, Procedure, Router, RouterDef } from './types.js';
2
+ /** A type-only RPC procedure declaration for a portable API contract. */
3
+ export interface RpcProcedureContract<TInput, TOutput> {
4
+ readonly '~rpc': {
5
+ readonly input: TInput;
6
+ readonly output: TOutput;
7
+ };
8
+ }
9
+ /** Client-facing view of a portable contract. */
10
+ export type InferRpcContractClient<TContract> = TContract extends RpcProcedureContract<infer TInput, infer TOutput> ? (input: TInput) => Promise<TOutput> : TContract extends object ? {
11
+ [K in keyof TContract]: InferRpcContractClient<TContract[K]>;
12
+ } : never;
13
+ /** Client-facing view of either a server router or a portable contract. */
14
+ export type InferRpcClient<TApi> = TApi extends {
15
+ readonly _tag: 'router';
16
+ readonly _def: RouterDef;
17
+ } ? InferRouterClient<TApi> : InferRpcContractClient<TApi>;
18
+ type RpcRouterDefinition<TContext, TContract> = {
19
+ readonly [K in keyof TContract]: TContract[K] extends RpcProcedureContract<infer TInput, infer TOutput> ? Procedure<TContext, TInput, TOutput> : TContract[K] extends object ? RpcRouterFor<TContext, TContract[K]> : never;
20
+ };
21
+ /** Server router shape required to implement a portable contract. */
22
+ export type RpcRouterFor<TContext, TContract> = Router<RpcRouterDefinition<TContext, TContract>>;
23
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { AnyRouter } from './types.js';
2
+ /**
3
+ * Dispatch an RPC POST. The `ctx` is already built (with its `ctx.auth`, for example)
4
+ * by the server before this is called. One request is one call,
5
+ * `{ method, input }`.
6
+ */
7
+ export declare function dispatchRpcRequest(args: {
8
+ router: AnyRouter;
9
+ ctx: unknown;
10
+ request: Request;
11
+ }): Promise<Response>;
@@ -0,0 +1,62 @@
1
+ import type { AuthContext, Scope } from '@fabrika/auth';
2
+ import type { StandardSchemaV1 } from '../standard-schema.js';
3
+ /** `query` and `mutation` are metadata today — the dispatcher treats them identically. */
4
+ export type ProcedureKind = 'query' | 'mutation';
5
+ /**
6
+ * A recorded authorization requirement. `scopeResolver` is declared as a METHOD
7
+ * (bivariant params) on purpose, so a concrete `Procedure<C, I, O>` stays
8
+ * assignable to `AnyProcedure`.
9
+ */
10
+ export interface AuthRequirement<TContext, TInput> {
11
+ readonly action: string;
12
+ scopeResolver?(input: TInput, ctx: TContext): Scope | undefined;
13
+ }
14
+ export interface Procedure<TContext, TInput, TOutput> {
15
+ readonly _tag: 'procedure';
16
+ readonly kind: ProcedureKind;
17
+ readonly input: StandardSchemaV1<unknown, TInput>;
18
+ readonly output: StandardSchemaV1<unknown, TOutput> | null;
19
+ readonly requirements: ReadonlyArray<AuthRequirement<TContext, TInput>>;
20
+ handler(args: {
21
+ ctx: TContext;
22
+ input: TInput;
23
+ }): Promise<TOutput> | TOutput;
24
+ }
25
+ export interface AnyProcedure {
26
+ readonly _tag: 'procedure';
27
+ readonly kind: ProcedureKind;
28
+ readonly input: StandardSchemaV1;
29
+ readonly output: StandardSchemaV1 | null;
30
+ readonly requirements: ReadonlyArray<AuthRequirement<unknown, unknown>>;
31
+ handler(args: {
32
+ ctx: unknown;
33
+ input: unknown;
34
+ }): Promise<unknown> | unknown;
35
+ }
36
+ export type RouterDef = {
37
+ readonly [key: string]: AnyProcedure | AnyRouter;
38
+ };
39
+ export interface Router<TDef extends RouterDef> {
40
+ readonly _tag: 'router';
41
+ readonly _def: TDef;
42
+ }
43
+ export type AnyRouter = Router<RouterDef>;
44
+ /** Re-export the auth contract used by procedure requirements. */
45
+ export type { AuthContext, Scope };
46
+ /** Server-facing view of a router: each procedure as its handler signature. */
47
+ export type InferRouter<TRouter> = TRouter extends {
48
+ _tag: 'router';
49
+ _def: infer TDef;
50
+ } ? TDef extends RouterDef ? {
51
+ [K in keyof TDef]: TDef[K] extends Procedure<infer C, infer I, infer O> ? (args: {
52
+ ctx: C;
53
+ input: I;
54
+ }) => Promise<O> | O : TDef[K] extends Router<RouterDef> ? InferRouter<TDef[K]> : never;
55
+ } : never : never;
56
+ /** Client-facing view of a router: each procedure as `(input) => Promise<output>`. */
57
+ export type InferRouterClient<TRouter> = TRouter extends {
58
+ _tag: 'router';
59
+ _def: infer TDef;
60
+ } ? TDef extends RouterDef ? {
61
+ [K in keyof TDef]: TDef[K] extends Procedure<unknown, infer I, infer O> ? (input: I) => Promise<O> : TDef[K] extends Router<RouterDef> ? InferRouterClient<TDef[K]> : never;
62
+ } : never : never;
@@ -0,0 +1,42 @@
1
+ import { type BodyAdmission, type InstallationCaller, type InstallationEndpointMethod, type InstallationEndpointRow, type InstallationEndpointTable } from '@fabrika/auth-core';
2
+ /** A row as the builder writes it; the record key of `endpoints` becomes its id. */
3
+ export type ServiceEndpointSpec = InstallationEndpointRow extends infer Row ? Row extends InstallationEndpointRow ? Omit<Row, 'id'> : never : never;
4
+ /** One installation service's table, with the row ids as a type so its handlers can be checked against them. */
5
+ export interface ServiceArtifact<Id extends string = string> extends InstallationEndpointTable {
6
+ /** The service's IAM app id; never `<app>@<env>`. */
7
+ readonly id: string;
8
+ readonly endpoints: readonly (InstallationEndpointRow & {
9
+ readonly id: Id;
10
+ })[];
11
+ }
12
+ export interface ServiceEndpointOptions {
13
+ readonly callers: readonly [InstallationCaller, ...InstallationCaller[]];
14
+ /** The request body the row admits; omitted admits none. */
15
+ readonly admission?: BodyAdmission;
16
+ /** The scopes an OAuth resource token must carry; required exactly when `callers` holds `oauth`. */
17
+ readonly scopes?: readonly string[];
18
+ }
19
+ export interface ServiceContractBuilder {
20
+ /** A liveness endpoint: anonymous, and served without `X-Fabrika-Endpoint`. */
21
+ health(method: InstallationEndpointMethod, path: string): ServiceEndpointSpec;
22
+ /** An endpoint the service authenticates itself; the proxy reads no credential and keeps `Authorization`. */
23
+ protocol(method: InstallationEndpointMethod, path: string): ServiceEndpointSpec;
24
+ /** A credentialed endpoint: a listed caller kind, and no permission (`authenticated`). */
25
+ service(method: InstallationEndpointMethod, path: string, options: ServiceEndpointOptions): ServiceEndpointSpec;
26
+ }
27
+ export interface ServiceContractDeclaration<Id extends string> {
28
+ /** `null` is a row this composition does not serve: the table has no row there, and its handler is never called. */
29
+ readonly endpoints: Readonly<Record<Id, ServiceEndpointSpec | null>>;
30
+ /** Literal prefixes a path under which reaches only a row declared under it. */
31
+ readonly reservedPrefixes: readonly string[];
32
+ }
33
+ export declare class ServiceContractError extends Error {
34
+ readonly name = "ServiceContractError";
35
+ readonly problems: readonly string[];
36
+ constructor(problems: readonly string[]);
37
+ }
38
+ /**
39
+ * Declare one installation service's endpoint table. Validated here, once: an id that is an application environment,
40
+ * an `oauth` caller without its scopes (or scopes without it), and every table the matcher would refuse.
41
+ */
42
+ export declare function defineServiceContract<const Id extends string>(id: string, declare: (s: ServiceContractBuilder) => ServiceContractDeclaration<Id>): ServiceArtifact<Id>;
@@ -0,0 +1,3 @@
1
+ export { dispatchRpcRequest } from '../rpc/dispatcher.js';
2
+ export { defineServiceContract, type ServiceArtifact, type ServiceContractBuilder, type ServiceContractDeclaration, ServiceContractError, type ServiceEndpointOptions, type ServiceEndpointSpec, } from './define.js';
3
+ export { serveService, type ServeServiceOptions, type ServiceHandler, type ServiceHandlers, ServiceHandlersError, type ServiceRequest, } from './serve.js';
@@ -0,0 +1,28 @@
1
+ import type { FetchService, RequestExecutionContext } from '../fetch.js';
2
+ import type { ServiceArtifact } from './define.js';
3
+ export interface ServiceRequest<Env> {
4
+ readonly request: Request;
5
+ /** The `:name` segments of the selected path, percent-decoded. */
6
+ readonly params: Readonly<Record<string, string>>;
7
+ readonly env: Env;
8
+ readonly exec: RequestExecutionContext;
9
+ }
10
+ export type ServiceHandler<Env> = (input: ServiceRequest<Env>) => Response | Promise<Response>;
11
+ /** One handler per declared row id, also for a row this composition left out; that one is never called. */
12
+ export type ServiceHandlers<Id extends string, Env> = {
13
+ readonly [K in Id]: ServiceHandler<Env>;
14
+ };
15
+ export interface ServeServiceOptions<Env> {
16
+ /** Map a value a handler threw to a response. Defaults to the structural error mapping. */
17
+ readonly onError?: (error: unknown, request: Request, env: Env) => Response;
18
+ }
19
+ export declare class ServiceHandlersError extends Error {
20
+ readonly name = "ServiceHandlersError";
21
+ }
22
+ /**
23
+ * Serve a service contract. Per request: a non-canonical path is a 400, a request no row selects a 404, a `service`
24
+ * row reached without the proxy's `X-Fabrika-Endpoint` for that row and its digest a 503; then the row's handler. A
25
+ * `protocol` or `health` row ignores the header, because the service authenticates it itself and it also arrives
26
+ * unfronted (ADR-0022 rule 5).
27
+ */
28
+ export declare function serveService<Id extends string, Env>(service: ServiceArtifact<Id>, handlers: ServiceHandlers<Id, Env>, options?: ServeServiceOptions<Env>): FetchService<Env>;
@@ -0,0 +1,55 @@
1
+ /** The Standard Schema interface. */
2
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
3
+ /** The Standard Schema properties. */
4
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
5
+ }
6
+ export declare namespace StandardSchemaV1 {
7
+ /** The Standard Schema properties interface. */
8
+ interface Props<Input = unknown, Output = Input> {
9
+ /** The version number of the standard. */
10
+ readonly version: 1;
11
+ /** The vendor name of the schema library. */
12
+ readonly vendor: string;
13
+ /** Validates unknown input values. */
14
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
15
+ /** Inferred types associated with the schema. */
16
+ readonly types?: Types<Input, Output> | undefined;
17
+ }
18
+ /** The result interface of the validate function. */
19
+ type Result<Output> = SuccessResult<Output> | FailureResult;
20
+ /** The result interface if validation succeeds. */
21
+ interface SuccessResult<Output> {
22
+ /** The typed output value. */
23
+ readonly value: Output;
24
+ /** The non-existent issues. */
25
+ readonly issues?: undefined;
26
+ }
27
+ /** The result interface if validation fails. */
28
+ interface FailureResult {
29
+ /** The issues of failed validation. */
30
+ readonly issues: ReadonlyArray<Issue>;
31
+ }
32
+ /** The issue interface of the failure output. */
33
+ interface Issue {
34
+ /** The error message of the issue. */
35
+ readonly message: string;
36
+ /** The path of the issue, if any. */
37
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
38
+ }
39
+ /** The path segment interface of the issue. */
40
+ interface PathSegment {
41
+ /** The key representing a path segment. */
42
+ readonly key: PropertyKey;
43
+ }
44
+ /** The Standard Schema types interface. */
45
+ interface Types<Input = unknown, Output = Input> {
46
+ /** The input type of the schema. */
47
+ readonly input: Input;
48
+ /** The output type of the schema. */
49
+ readonly output: Output;
50
+ }
51
+ /** Infers the input type of a Standard Schema. */
52
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
53
+ /** Infers the output type of a Standard Schema. */
54
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
55
+ }
@@ -0,0 +1,95 @@
1
+ import type { PermissionEntry, PrincipalAffiliation } from '@fabrika/auth-core';
2
+ import type { ActionOf, AnyContract, DimensionOf, RoleOf } from '../contract/define';
3
+ /** `app` is the connector of another application: its key reaches only the exports that application consumes. */
4
+ export type TestCallerKind = 'human' | 'machine' | 'link' | 'app';
5
+ export interface TestScope<D extends string = string> {
6
+ readonly type: D;
7
+ readonly value: string;
8
+ }
9
+ /** An action, or a role whose actions IAM grants together; without a scope it is global. */
10
+ export type TestGrant<C> = {
11
+ readonly action: ActionOf<C>;
12
+ readonly scope?: TestScope<DimensionOf<C>>;
13
+ } | {
14
+ readonly role: RoleOf<C>;
15
+ readonly scope?: TestScope<DimensionOf<C>>;
16
+ };
17
+ export interface TestCallerOptions {
18
+ /** Defaults to `human`. */
19
+ readonly kind?: TestCallerKind;
20
+ /** How a human signs in: the IAM session (default) or a `px_` key bound to the person. Other kinds use a key. */
21
+ readonly via?: 'session' | 'key';
22
+ readonly id?: string;
23
+ readonly label?: string;
24
+ /** A human's address (`ctx.principal.email`), normalized as IAM stores it. Only a human has one. */
25
+ readonly email?: string;
26
+ /**
27
+ * Defaults to `internal`. An `external` human holds only roles the contract declares `external`, as IAM resolves it
28
+ * (ADR-0051); the kit refuses any other grant for one.
29
+ */
30
+ readonly affiliation?: PrincipalAffiliation;
31
+ /**
32
+ * An operator disabled the principal in IAM: a member directory lists and finds it with `disabled: true`, and the kit's
33
+ * IAM signs no token for its session or key, so its requests are refused as IAM refuses a disabled principal's.
34
+ */
35
+ readonly disabled?: boolean;
36
+ /**
37
+ * When the person last signed in to the application (unix seconds), as IAM records it at the handoff; absent, never.
38
+ * A member directory answers it to a holder of `members.manage` only. Only a human signs in.
39
+ */
40
+ readonly lastSignInAt?: number;
41
+ /** The consuming application's id, for an `app` caller. */
42
+ readonly app?: string;
43
+ /**
44
+ * What a `link` caller's key is bound to: the link mount, and the object first, then each value containing it. A link
45
+ * caller without one holds a key bound to nothing, which no procedure admits.
46
+ */
47
+ readonly link?: TestLink;
48
+ }
49
+ export interface TestLink {
50
+ readonly mount: string;
51
+ readonly chain: readonly TestScope[];
52
+ }
53
+ interface TestCallerCredential {
54
+ /** The principal id, or the share key's credential id for a link. */
55
+ readonly id: string;
56
+ /** `internal` for every caller but an `external` human. */
57
+ readonly affiliation: PrincipalAffiliation;
58
+ /** `true` for a principal an operator disabled; `false` for every other caller, a link holder included. */
59
+ readonly disabled: boolean;
60
+ /** A human's last sign-in to the application (`TestCallerOptions.lastSignInAt`); `null` for one that never did and every other caller. */
61
+ readonly lastSignInAt: number | null;
62
+ readonly perms: readonly PermissionEntry[];
63
+ /** The `px_` key a key caller presents as a bearer; `null` for a session. */
64
+ readonly key: string | null;
65
+ /** The `Cookie` header a signed-in browser sends; `null` for a key caller. */
66
+ readonly cookie: string | null;
67
+ }
68
+ /** A person, a script or another application's connector: a principal with a label. */
69
+ export interface LabelledTestCaller extends TestCallerCredential {
70
+ readonly kind: Exclude<TestCallerKind, 'link'>;
71
+ /** The consuming application of an `app` caller, `null` for every other kind. */
72
+ readonly app: string | null;
73
+ readonly link: null;
74
+ readonly label: string;
75
+ /** A human's normalized address, which its token carries; `null` for a script and a connector. */
76
+ readonly email: string | null;
77
+ }
78
+ /** A person, whose token carries its address: `ctx.principal.email` of its requests. */
79
+ export interface PersonTestCaller extends LabelledTestCaller {
80
+ readonly kind: 'human';
81
+ readonly email: string;
82
+ }
83
+ /** The holder of a share link: an anonymous key, which carries no label. */
84
+ export interface LinkTestCaller extends TestCallerCredential {
85
+ readonly kind: 'link';
86
+ readonly app: null;
87
+ /** The link mount and chain the key is bound to; `null` for a key bound to nothing. */
88
+ readonly link: TestLink | null;
89
+ readonly label: null;
90
+ readonly email: null;
91
+ }
92
+ export type TestCaller = LabelledTestCaller | LinkTestCaller;
93
+ /** The permissions `grants` resolve to: a role becomes each of its actions in the role's scope. */
94
+ export declare function resolveGrants<C extends AnyContract>(contract: C, grants: readonly TestGrant<C>[]): PermissionEntry[];
95
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { RpcFetch } from '../client/transport';
2
+ import type { ChallengeFetch } from '../implement/challenge';
3
+ /** The token Turnstile's always-passing test widget (site key `1x00000000000000000000AA`) hands out every time. */
4
+ export declare const TURNSTILE_TEST_TOKEN = "XXXX.DUMMY.TOKEN.XXXX";
5
+ /** Turnstile's siteverify that passes every token: a fresh answer for the host and the form the check expects. */
6
+ export declare const passingChallenge: ChallengeFetch;
7
+ /**
8
+ * Fetches a challenge from `url` through `fetch` and solves it as the widget does, answering the payload the page puts
9
+ * into `challengeToken`. The kit issues the cheapest challenge, so this takes milliseconds.
10
+ */
11
+ export declare function solveChallengeAt(fetch: RpcFetch, url: string): Promise<string>;
@@ -0,0 +1,97 @@
1
+ import { type EmailDeliveryErrorCode, type EmailDeliveryOutcome } from '@fabrika/email';
2
+ import type { AnyContract, DimensionOf } from '../contract/define';
3
+ import type { ContractImplementation } from '../implement/implement';
4
+ import type { PersonTestCaller, TestCaller } from './caller';
5
+ import { type TestKit, type TestKitOptions } from './kit';
6
+ import type { SentInvitation, TestMembership } from './membership';
7
+ /** Someone the author clicks through the application as: a person the seed made with `dev.testCaller`. */
8
+ export interface DevPersona {
9
+ /** Shown on the picker's card and in the toolbar, unique among the personas: `Petra`. */
10
+ readonly name: string;
11
+ /** One short line of who this is: `Staff of the design workspace`. */
12
+ readonly description?: string;
13
+ readonly caller: PersonTestCaller;
14
+ }
15
+ /** A page a link holder opens: a link the seed issued through the application's own endpoint. */
16
+ export interface DevLink {
17
+ readonly title: string;
18
+ readonly description?: string;
19
+ /** The URL the application answered, a path of its origin: `/s/#px_…`. */
20
+ readonly url: string;
21
+ }
22
+ /** A scope value the seed made, with the name a person knows it by: `{ value: 'w1', label: 'Design team' }`. */
23
+ export interface DevScopeValue {
24
+ readonly value: string;
25
+ readonly label: string;
26
+ }
27
+ /** What a seed answers; every part is optional. */
28
+ export interface DevSeedResult<C extends AnyContract> {
29
+ readonly personas?: readonly DevPersona[];
30
+ readonly links?: readonly DevLink[];
31
+ /** The values of each dimension the seed made, which the picker offers for a membership role. */
32
+ readonly scopes?: {
33
+ readonly [D in DimensionOf<C>]?: readonly DevScopeValue[];
34
+ };
35
+ }
36
+ /** The origin `fabrika app dev` captures every request to: an outside provider's stand-in, named by its path. */
37
+ export declare const DEV_OUTBOX_ORIGIN = "https://outbox.invalid";
38
+ /** A request the application sent to `DEV_OUTBOX_ORIGIN` under `fabrika app dev`: its body, never its headers. */
39
+ export interface DevOutboxMessage {
40
+ readonly id: number;
41
+ readonly method: string;
42
+ /** The path it was sent to: `/mail`. */
43
+ readonly path: string;
44
+ readonly contentType: string | null;
45
+ readonly body: string;
46
+ }
47
+ /** A message a job handed to the kit's mail stand-in, as `fabrika app dev` shows it. */
48
+ export interface DevMail {
49
+ /** Its position among the handoffs, from 1. */
50
+ readonly id: number;
51
+ /** When it was handed off, ISO 8601. */
52
+ readonly at: string;
53
+ readonly to: string;
54
+ readonly subject: string;
55
+ readonly text: string;
56
+ readonly html: string | null;
57
+ readonly replyTo: string | null;
58
+ readonly fromName: string | null;
59
+ /** The idempotency key as the job passed it. */
60
+ readonly key: string;
61
+ /** The key the transport got: `key` behind the application environment's prefix. */
62
+ readonly transportKey: string;
63
+ readonly answer: 'accepted' | EmailDeliveryErrorCode;
64
+ readonly outcome: 'accepted' | EmailDeliveryOutcome;
65
+ /** Whether this handoff put the message in the inbox; a repeat under its key does not. */
66
+ readonly delivered: boolean;
67
+ }
68
+ /** What `fabrika app dev` stands in for outside the application, for the seed to read. */
69
+ export interface DevOutside {
70
+ /** Every request sent to `DEV_OUTBOX_ORIGIN` so far, oldest first. */
71
+ outbox(): readonly DevOutboxMessage[];
72
+ /** Every message a job handed to the mail stand-in so far, oldest first. */
73
+ mails(): readonly DevMail[];
74
+ }
75
+ /**
76
+ * The default export of `dev/seed.ts`: it receives the application's test kit, the API its tests use, writes through the
77
+ * endpoints, and answers who and what the picker offers. `outside` reads what the application sent to the dev outbox
78
+ * and every message a job handed to the mail stand-in, oldest first.
79
+ */
80
+ export type DevSeed<C extends AnyContract> = (dev: TestKit<C>, outside: DevOutside) => Promise<DevSeedResult<C>> | DevSeedResult<C>;
81
+ export interface DevKit<C extends AnyContract> {
82
+ readonly kit: TestKit<C>;
83
+ /** The person `value` is when this kit made it; `undefined` for any other value. */
84
+ personOf(value: unknown): PersonTestCaller | undefined;
85
+ /** The roles a caller of this kit holds now: its grants, with what the application assigned and revoked through IAM. */
86
+ membershipsOf(caller: TestCaller): readonly TestMembership[];
87
+ /** The invitation e-mails the stand-in IAM sent, in the order it first sent them. */
88
+ invitations(): readonly SentInvitation[];
89
+ /** Every message the application's jobs handed to the mail stand-in, oldest first; one refused before the handoff is absent. */
90
+ mails(): readonly DevMail[];
91
+ }
92
+ /**
93
+ * The test kit of `app` with its stand-in IAM, and what `fabrika app dev` shows of it. A challenge passes
94
+ * (`passingChallenge`) unless `options.challengeFetch` answers it, and admits the test widget's token
95
+ * (`TURNSTILE_TEST_TOKEN`) at every call; any other token is single use, as deployed.
96
+ */
97
+ export declare function devKit<C extends AnyContract>(app: ContractImplementation<C>, options?: Omit<TestKitOptions, 'membership'>): DevKit<C>;
@@ -0,0 +1,99 @@
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
+ export interface EndpointAccessKit {
32
+ /**
33
+ * A caller holding the endpoint's action in one scope names an object (`on`) or a scope value (`in`) of another;
34
+ * `input` is the rest of a valid input, and its `from` field is replaced.
35
+ */
36
+ denied(endpoint: string, input: Readonly<Record<string, unknown>>, seed?: Seed): Promise<DeniedOutcome>;
37
+ /**
38
+ * A caller of the other scope asks first, then a caller of one scope asks for its own object (`on`), its list
39
+ * (`over`) or, for every table that lies in the dimension, its scope value (`in`).
40
+ */
41
+ leaked(endpoint: string, input: Readonly<Record<string, unknown>>, seed?: Seed): Promise<LeakOutcome>;
42
+ /**
43
+ * A caller that holds only the endpoint's action asks for its own object: a person or a script in its scope, or, on
44
+ * a link mount, the holder of a link bound to the object. A write action reads its own target. Where IAM requires more
45
+ * of the caller, it holds that too: a member manager an assigning role and the assigned role's actions, a link's
46
+ * issuer the actions the link grants.
47
+ */
48
+ own(endpoint: string, input: Readonly<Record<string, unknown>>, seed?: Seed): Promise<DeniedOutcome>;
49
+ /**
50
+ * An `app` or a `creates` endpoint asked by a caller who holds its action in one value of a dimension only, never
51
+ * globally: refused (403), by the proxy for `app` and by IAM for `creates`, since both need a global grant.
52
+ */
53
+ scoped(endpoint: string, input: Readonly<Record<string, unknown>>): Promise<DeniedOutcome>;
54
+ /**
55
+ * An `app` or a `creates` endpoint asked by a caller who holds its action as a global grant, and nothing else:
56
+ * `answered` unless the answer is a 401, a 403 or a 5xx.
57
+ */
58
+ global(endpoint: string, input: Readonly<Record<string, unknown>>): Promise<ReachOutcome>;
59
+ /**
60
+ * One capability of an endpoint of several, refused while every other one passes: the caller holds each other one's
61
+ * action in a scope of its own and addresses its object or value there (or, for `over`, holds the action globally).
62
+ * For an `on` or `in` capability it also holds the named one's action in its own scope and addresses the other's
63
+ * object (404) or value (403); for `over` it holds the named action nowhere (403). `seed` is the named one's.
64
+ */
65
+ capability(endpoint: string, capability: string, input: Readonly<Record<string, unknown>>, seed?: Seed): Promise<DeniedOutcome>;
66
+ /**
67
+ * A public procedure asked by anyone, with no credential, from a page of its own origin: `answered` unless the answer
68
+ * is a 401, a 403, a 5xx or a failed challenge. A procedure with a challenge is a 400 `challenge_failed`: the kit's
69
+ * challenge never passes.
70
+ */
71
+ anyone(endpoint: string, input: Readonly<Record<string, unknown>>): Promise<ReachOutcome>;
72
+ }
73
+ export type Grant = {
74
+ readonly action: string;
75
+ readonly scope: {
76
+ readonly type: string;
77
+ readonly value: string;
78
+ };
79
+ } | {
80
+ readonly role: string;
81
+ readonly scope: {
82
+ readonly type: string;
83
+ readonly value: string;
84
+ };
85
+ };
86
+ /**
87
+ * The least a member manager holds in `scope` for IAM to let it give or remove the roles a case may ask for: the
88
+ * endpoint's action, one assigning role of each (`assignableBy`) that adds the fewest actions beyond those, and the
89
+ * actions of each as plain grants. The roles a case asks for are the membership roles its input names; an input that
90
+ * names none may ask for any, so the caller then covers every membership role of the dimension. Where one role assigns
91
+ * them all, it is the only role held. Every role is held in `scope`, an application-wide assigner too: IAM counts any
92
+ * assigning role held there, and a grant in `scope` reaches no other value.
93
+ */
94
+ export declare const managerGrants: (artifact: Pick<ContractArtifact, "roles">, action: string, input: Readonly<Record<string, unknown>>, scope: Scope) => Grant[];
95
+ /**
96
+ * The endpoint access kit of `app`, whose data layer is `scoped(tables)`, over `postgres`. It boots `app` once, so the
97
+ * application's layer is bound here and nowhere else in the process.
98
+ */
99
+ export declare function endpointAccessKit<DB, P>(app: ContractImplementation<AnyContract>, tables: Tables<DB, P>, postgres: TestDatabase): 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 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';