Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function UpdateControl({ post }) {
);
}

<StackProvider auth={clientAuth} /* router, api, overrides */>
<StackProvider stack={clientStack} auth={clientAuth} /* router, overrides */>
{children}
</StackProvider>;
```
Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/plugins/cms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -751,9 +751,10 @@ const auth = createClientAuth({
})

<StackProvider
stack={stack}
auth={auth}
initialIdentity={initialIdentity}
// router, api, overrides, ...
// router, overrides, ...
>
{children}
</StackProvider>
Expand Down
8 changes: 6 additions & 2 deletions docs/content/docs/plugins/form-builder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,11 @@ const clientAuth = createClientAuth({
loginPath: "/auth/sign-in",
})

<StackProvider auth={clientAuth} initialIdentity={initialIdentity}>
<StackProvider
stack={clientStack}
auth={clientAuth}
initialIdentity={initialIdentity}
>
{children}
</StackProvider>
```
Expand Down Expand Up @@ -662,7 +666,7 @@ Creates the client plugin with routes and SSR loaders.
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `fieldComponents` | `Record<string, ComponentType>` | No | Custom field components |
| `localization` | `FormBuilderLocalization` | No | Custom labels |
| `localization` | `Partial<FormBuilderLocalization>` | No | Custom labels |
| `showAttribution` | `boolean` | No | Show BTST attribution |
| `onRouteRender` | `(route, context) => void` | No | Lifecycle hook |
| `onRouteError` | `(route, error, context) => void` | No | Error hook |
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/plugins/kanban.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -618,11 +618,11 @@ All built-in Kanban component copy is routed through the `i18n` provider on `Sta

```tsx
<StackProvider
stack={clientStack}
i18n={{
translate: (key, defaultValue, params) =>
i18next.t(key, { defaultValue, ...params }),
}}
// ...
/>
```

Expand Down
42 changes: 42 additions & 0 deletions docs/v3-contract-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# v3 all-plugin contract audit

This matrix is the completion record for issue #225. Programmatic IDs are
camelCase identifiers; kebab-case package exports and URL slugs remain
unchanged. Paths beginning with `src/` or `consumer-tests/` are relative to
`packages/stack/`.

| Surface | Final contract | Focused evidence |
| --- | --- | --- |
| AI Chat (`aiChat`) | Options-object backend preserves model and tools; the runtime-independent client factory preserves public/authenticated mode and resolves all loader/browser transport from the client stack. | `src/plugins/ai-chat/__tests__/authorization.test.ts`, `client-sweep.test.tsx`, `chat-hooks.test.tsx`, and `src/__tests__/ai-chat-client-runtime.test.ts` |
| Blog (`blog`) | Options-object backend and nested lifecycle hooks remain; loaders, metadata, sitemap, hydration, queries, and mutations share the resolved runtime. | `src/plugins/blog/__tests__/authorization.test.ts`, `client-runtime.test.ts`, `client-runtime.browser.test.tsx`, and `client-sweep.test.tsx` |
| CMS (`cms`) | Content-type configuration remains factory-owned; request operations, trusted operations, raw helpers, client runtime, and provider behavior stay separated. | `src/plugins/cms/__tests__/authorization.test.ts`, `schema-roundtrip.test.ts`, `client-runtime.test.ts`, `client-runtime.browser.test.tsx`, and `client-sweep.test.tsx` |
| Comments (`comments`) | Flags, moderation, and user-resolution behavior remain; backend hooks use the nested grammar and client data uses the resolved runtime. | `src/plugins/comments/__tests__/authorization.test.ts`, `client-runtime.test.ts`, `client-runtime.browser.test.tsx`, and `client-sweep.test.tsx` |
| Form Builder (`formBuilder`) | Form/submission operation lifecycles, loaders, browser mutations, authorization gates, and provider services retain parity. | `src/plugins/form-builder/__tests__/authorization.test.ts`, `client-runtime.test.ts`, `client-runtime.browser.test.tsx`, `client-sweep.test.tsx`, and `form-renderer-authorization.test.tsx` |
| Kanban (`kanban`) | The options-object backend preserves user search/resolution and board/column/task lifecycles; protected query keys and browser runtime are identity-partitioned. | `src/plugins/kanban/__tests__/authorization.test.ts`, `client-runtime.test.ts`, `client-runtime.browser.test.tsx`, `identity-partition.test.tsx`, and `client-sweep.test.tsx` |
| Media (`media`) | Storage and tenant state stay server-side; upload lifecycle, cross-origin isolation, identity partitions, hydration, and browser mutation behavior use the resolved runtime without secret leakage. | `src/plugins/media/__tests__/authorization.test.ts`, `storage-adapters.test.ts`, `client-runtime.test.ts`, `client-runtime.browser.test.tsx`, `identity-partition.test.tsx`, and `client-sweep.test.tsx` |
| OpenAPI (`openApi`) | Intentionally backend-only; schema/reference routes use validated endpoint inventory and emit deterministic safe access metadata. | `src/plugins/open-api/__tests__/route-introspection.test.ts` and `src/__tests__/endpoint-inventory.test.ts` |
| Route Docs (`routeDocs`) | Intentionally client-only; the definition inherits site/query runtime and inspects resolved client routes by canonical ID. | `src/plugins/route-docs/client/plugin.tsx`, `src/plugins/route-docs/generator.ts`, and `src/__tests__/client-runtime.test.ts` |
| UI Builder (`uiBuilder`) | Intentionally client-only over CMS; component registration stays factory-owned while provider overrides are inferred from the registered definition. | `src/plugins/ui-builder/__tests__/client-sweep.test.tsx`, `src/plugins/ui-builder/client/plugin.tsx`, and `src/__tests__/plugin-registration.typecheck.tsx` |
| Third-party consumer | Public backend/client definitions require stable IDs, preserve route/operation inference, and reject mismatched IDs, overrides, configs, and auth catalogs without internal imports. | `consumer-tests/constructor-exports`, `src/__tests__/plugins.test.tsx`, `plugin-registration.test.tsx`, and `plugin-registration.typecheck.tsx` |

## Cross-cutting contract evidence

- `src/__tests__/stack-constructors.test.ts` and
`package-metadata.test.ts` prove that only `createBackendStack` and
`createClientStack` exist across source, declarations, ESM, and CJS.
- `src/__tests__/client-runtime.test.ts` covers one runtime, same-origin path
overrides, complete endpoint replacement, sensitive-header isolation,
provider projection, SSR/browser hydration, and rejected unqualified runtime
fields.
- `src/__tests__/plugin-registration.test.tsx` covers canonical IDs, mismatch and
duplicate failures, provider inference, and route diagnostics.
- `src/__tests__/authorization.test.ts`, `authorization.typecheck.ts`, and the
plugin authorization suites cover permissive omitted auth, authoritative
enabled auth, trusted fact derivation, `trusted`/`raw` boundaries, immutable
results, and lifecycle behavior.
- `src/__tests__/initial-identity-hydration.test.tsx` and
`initial-identity-layouts.test.tsx` cover tri-state request hydration across
Next.js, React Router, and TanStack helpers.
- `scripts/check-canonical-dx.mjs`, package consumer compiles, strict package
metadata tests, registry generation/install tests, generated app builds, and
root Node 22 verification are the release gates for this matrix.
18 changes: 18 additions & 0 deletions packages/stack/consumer-tests/client-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@ observations[0]?.id satisfies string | undefined;
// @ts-expect-error Request headers do not exist on the top-level provider projection.
clientStack.provider.api.headers;

createClientStack({
// @ts-expect-error Shared origins must be qualified under api or site.
baseURL: "https://ignored.example.com",
api: { baseURL: "https://app.example.com", basePath: "/api/data" },
site: { baseURL: "https://app.example.com", basePath: "/pages" },
queryClient,
plugins: { consumerProbe: probeClientPlugin() },
});

createClientStack({
// @ts-expect-error Shared paths must be qualified under api or site.
basePath: "/ignored",
api: { baseURL: "https://app.example.com", basePath: "/api/data" },
site: { baseURL: "https://app.example.com", basePath: "/pages" },
queryClient,
plugins: { consumerProbe: probeClientPlugin() },
});

createClientStack({
api: { baseURL: "https://app.example.com", basePath: "/api/data" },
site: { baseURL: "https://app.example.com", basePath: "/pages" },
Expand Down
35 changes: 31 additions & 4 deletions packages/stack/consumer-tests/client-runtime/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,41 @@ import { browserClientStack } from "./index";

<StackProvider stack={browserClientStack} />;

// @ts-expect-error Override values are inferred from the registered plugin factory.
// @ts-expect-error Provider paths come from the resolved stack.
<StackProvider stack={browserClientStack} basePath="/ignored" />;

// @ts-expect-error Provider API configuration comes from the resolved stack.
<StackProvider stack={browserClientStack} api={{ basePath: "/ignored" }} />;

const propsWithLegacyBasePath = {
stack: browserClientStack,
basePath: "/ignored",
};
// @ts-expect-error Spread provider props cannot override resolved stack paths.
<StackProvider {...propsWithLegacyBasePath} />;

const propsWithLegacyApi = {
stack: browserClientStack,
api: { basePath: "/ignored" },
};
// @ts-expect-error Spread provider props cannot override resolved stack API configuration.
<StackProvider {...propsWithLegacyApi} />;

<StackProvider
stack={browserClientStack}
overrides={{ consumerProbe: { label: "Consumer", format: "wide" } }}
overrides={{
consumerProbe: {
label: "Consumer",
// @ts-expect-error Override values are inferred from the registered plugin factory.
format: "wide",
},
}}
/>;

// @ts-expect-error Unknown plugins are not valid provider override keys.
<StackProvider
stack={browserClientStack}
overrides={{ missing: { label: "Missing" } }}
overrides={{
// @ts-expect-error Unknown plugins are not valid provider override keys.
missing: { label: "Missing" },
}}
/>;
42 changes: 14 additions & 28 deletions packages/stack/consumer-tests/constructor-exports/index.cts
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
import api = require("@btst/stack/api");
import client = require("@btst/stack/client");

type Equal<TLeft, TRight> = (<T>() => T extends TLeft ? 1 : 2) extends <
T,
>() => T extends TRight ? 1 : 2
? true
: false;
type Expect<T extends true> = T;

type _BackendFactoriesMatch = Expect<
Equal<typeof api.createBackendStack, typeof api.stack>
>;
type _ClientFactoriesMatch = Expect<
Equal<typeof client.createClientStack, typeof client.createStackClient>
>;
type _BackendConfigAliasMatches = Expect<
Equal<api.BackendStackConfig, api.BackendLibConfig>
>;
type _BackendResultAliasMatches = Expect<
Equal<api.BackendStack, api.BackendLib>
>;
type _ClientConfigAliasMatches = Expect<
Equal<client.ClientStackConfig, client.ClientLibConfig>
>;
type _ClientResultAliasMatches = Expect<
Equal<client.ClientStack, client.ClientLib>
>;

void api.createBackendStack;
void api.stack;
void client.createClientStack;
void client.createStackClient;

type BackendConfig = api.BackendStackConfig;
type BackendResult = api.BackendStack;
type ClientConfig = client.ClientStackConfig;
type ClientResult = client.ClientStack;

declare const backendConfig: BackendConfig;
declare const clientConfig: ClientConfig;
declare const backendResult: BackendResult;
declare const clientResult: ClientResult;
void backendConfig;
void clientConfig;
void backendResult;
void clientResult;
106 changes: 23 additions & 83 deletions packages/stack/consumer-tests/constructor-exports/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
import type { DatabaseDefinition, DBAdapter } from "@btst/db";
import { QueryClient } from "@tanstack/react-query";
import { z } from "zod";
import {
createBackendStack,
stack,
type BackendLib,
type BackendLibConfig,
type BackendStack,
type BackendStackConfig,
} from "@btst/stack/api";
import { createBackendStack, type BackendStackConfig } from "@btst/stack/api";
import {
defineAuthorization,
definePermissions,
permission,
} from "@btst/stack/authorization";
import { createServerAuth } from "@btst/stack/authorization/server";
import {
createClientStack,
createStackClient,
type ClientLib,
type ClientLibConfig,
type ClientStack,
type ClientStackConfig,
} from "@btst/stack/client";
import { createClientStack, type ClientStackConfig } from "@btst/stack/client";
import {
createDbPlugin,
createEndpoint,
Expand Down Expand Up @@ -56,7 +43,7 @@ const read = defineOperation({
execute: ({ input }) => ({ id: input.id }),
});
const backendPlugin = defineBackendPlugin({
name: "consumerProbe",
id: "consumerProbe",
dbPlugin: createDbPlugin("consumerProbe", {}),
operations: () => ({ read }),
routes: (_adapter, _context, operations) => ({
Expand All @@ -76,41 +63,14 @@ const backendConfig = {
{ consumerProbe: typeof backendPlugin },
typeof auth
>;
const canonicalBackend = createBackendStack(backendConfig);
const temporaryBackend = stack(backendConfig);
const backend = createBackendStack(backendConfig);

type _BackendFactoriesMatch = Expect<
Equal<typeof createBackendStack, typeof stack>
>;
type _BackendInferenceMatches = Expect<
Equal<typeof canonicalBackend, typeof temporaryBackend>
>;
type _BackendRouteInference = Expect<
Equal<keyof typeof canonicalBackend.router.endpoints, "consumerProbe_read">
Equal<keyof typeof backend.router.endpoints, "consumerProbe_read">
>;
type _BackendConfigAliasMatches = Expect<
Equal<
BackendStackConfig<{ consumerProbe: typeof backendPlugin }, typeof auth>,
BackendLibConfig<{ consumerProbe: typeof backendPlugin }, typeof auth>
>
>;
type _BackendResultAliasMatches = Expect<
Equal<
BackendStack<
ReturnType<typeof backendPlugin.routes>,
Record<string, never>,
typeof canonicalBackend.trusted
>,
BackendLib<
ReturnType<typeof backendPlugin.routes>,
Record<string, never>,
typeof canonicalBackend.trusted
>
>
>;
canonicalBackend.router.endpoints.consumerProbe_read;
canonicalBackend.trusted.consumerProbe.read({ id: "record-1" });
canonicalBackend
backend.router.endpoints.consumerProbe_read;
backend.trusted.consumerProbe.read({ id: "record-1" });
backend
.forRequest(new Request("https://example.com/api"))
.operations.consumerProbe.read({ id: "record-1" });

Expand All @@ -127,46 +87,26 @@ const incompatibleAuth = createServerAuth({
});
createBackendStack({
...backendConfig,
// @ts-expect-error public canonical constructor rejects incompatible catalogs
auth: incompatibleAuth,
});
stack({
...backendConfig,
// @ts-expect-error public temporary constructor preserves catalog inference
// @ts-expect-error the public constructor rejects incompatible catalogs
auth: incompatibleAuth,
});

const clientPlugin = defineClientPlugin({
name: "consumerProbe",
routes: () => ({
read: createRoute("/consumer-probe/:id", ({ params }) => ({
PageComponent: () => null,
loader: async () => ({ recordId: params.id }),
})),
id: "consumerProbe",
resolve: () => ({
routes: () => ({
read: createRoute("/consumer-probe/:id", ({ params }) => ({
PageComponent: () => null,
loader: async () => ({ recordId: params.id }),
})),
}),
}),
});
const clientConfig = {
api: { baseURL: "https://example.com", basePath: "/api" },
site: { baseURL: "https://example.com", basePath: "/pages" },
queryClient: new QueryClient(),
plugins: { consumerProbe: clientPlugin },
} satisfies ClientStackConfig<{ consumerProbe: typeof clientPlugin }>;
const canonicalClient = createClientStack(clientConfig);
const temporaryClient = createStackClient(clientConfig);

type _ClientFactoriesMatch = Expect<
Equal<typeof createClientStack, typeof createStackClient>
>;
type _ClientInferenceMatches = Expect<
Equal<typeof canonicalClient, typeof temporaryClient>
>;
type _ClientConfigAliasMatches = Expect<
Equal<
ClientStackConfig<{ consumerProbe: typeof clientPlugin }>,
ClientLibConfig<{ consumerProbe: typeof clientPlugin }>
>
>;
type _ClientResultAliasMatches = Expect<
Equal<
ClientStack<ReturnType<typeof clientPlugin.routes>>,
ClientLib<ReturnType<typeof clientPlugin.routes>>
>
>;
canonicalClient.router.getRoute("/consumer-probe/record-1")?.loader?.();
const client = createClientStack(clientConfig);
client.router.getRoute("/consumer-probe/record-1")?.loader?.();
4 changes: 2 additions & 2 deletions packages/stack/registry/btst-ai-chat.json

Large diffs are not rendered by default.

Loading