every-plugin 3.0.0-rc.15 → 3.0.0-rc.16

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 (65) hide show
  1. package/dist/build/artifact-names.cjs +8 -5
  2. package/dist/build/artifact-names.cjs.map +1 -1
  3. package/dist/build/artifact-names.d.cts +7 -5
  4. package/dist/build/artifact-names.d.cts.map +1 -1
  5. package/dist/build/artifact-names.d.mts +7 -5
  6. package/dist/build/artifact-names.d.mts.map +1 -1
  7. package/dist/build/artifact-names.mjs +8 -5
  8. package/dist/build/artifact-names.mjs.map +1 -1
  9. package/dist/build/ui/stubs.cjs +2 -7
  10. package/dist/build/ui/stubs.cjs.map +1 -1
  11. package/dist/build/ui/stubs.d.cts.map +1 -1
  12. package/dist/build/ui/stubs.d.mts.map +1 -1
  13. package/dist/build/ui/stubs.mjs +2 -7
  14. package/dist/build/ui/stubs.mjs.map +1 -1
  15. package/dist/errors.cjs +11 -1
  16. package/dist/errors.cjs.map +1 -1
  17. package/dist/errors.d.cts +15 -1
  18. package/dist/errors.d.cts.map +1 -1
  19. package/dist/errors.d.mts +15 -1
  20. package/dist/errors.d.mts.map +1 -1
  21. package/dist/errors.mjs +11 -2
  22. package/dist/errors.mjs.map +1 -1
  23. package/dist/index.cjs +1 -0
  24. package/dist/index.d.cts +2 -2
  25. package/dist/index.d.mts +2 -2
  26. package/dist/index.mjs +2 -2
  27. package/dist/runtime/mf-config.cjs +1 -1
  28. package/dist/runtime/mf-config.mjs +1 -1
  29. package/dist/ui/manifest/construct.cjs +13 -38
  30. package/dist/ui/manifest/construct.cjs.map +1 -1
  31. package/dist/ui/manifest/construct.d.cts +4 -13
  32. package/dist/ui/manifest/construct.d.cts.map +1 -1
  33. package/dist/ui/manifest/construct.d.mts +4 -13
  34. package/dist/ui/manifest/construct.d.mts.map +1 -1
  35. package/dist/ui/manifest/construct.mjs +14 -39
  36. package/dist/ui/manifest/construct.mjs.map +1 -1
  37. package/dist/ui/manifest/descriptor.cjs +1 -1
  38. package/dist/ui/manifest/descriptor.mjs +1 -1
  39. package/dist/ui/manifest/index.d.cts +3 -3
  40. package/dist/ui/manifest/index.d.mts +3 -3
  41. package/dist/ui/manifest/mount-registry.cjs +6 -13
  42. package/dist/ui/manifest/mount-registry.cjs.map +1 -1
  43. package/dist/ui/manifest/mount-registry.d.cts +29 -36
  44. package/dist/ui/manifest/mount-registry.d.cts.map +1 -1
  45. package/dist/ui/manifest/mount-registry.d.mts +29 -36
  46. package/dist/ui/manifest/mount-registry.d.mts.map +1 -1
  47. package/dist/ui/manifest/mount-registry.mjs +6 -13
  48. package/dist/ui/manifest/mount-registry.mjs.map +1 -1
  49. package/dist/version-manifest.cjs +25 -0
  50. package/dist/version-manifest.cjs.map +1 -1
  51. package/dist/version-manifest.d.cts +13 -1
  52. package/dist/version-manifest.d.cts.map +1 -1
  53. package/dist/version-manifest.d.mts +13 -1
  54. package/dist/version-manifest.d.mts.map +1 -1
  55. package/dist/version-manifest.mjs +24 -1
  56. package/dist/version-manifest.mjs.map +1 -1
  57. package/package.json +1 -1
  58. package/skills/plugin-client/SKILL.md +2 -2
  59. package/src/build/artifact-names.ts +8 -5
  60. package/src/build/ui/stubs.ts +2 -7
  61. package/src/errors.ts +10 -0
  62. package/src/ui/manifest/construct.ts +20 -65
  63. package/src/ui/manifest/index.ts +0 -2
  64. package/src/ui/manifest/mount-registry.ts +20 -20
  65. package/src/version-manifest.ts +27 -0
@@ -1,9 +1,9 @@
1
1
  import type { AnyRoute } from "@tanstack/react-router";
2
- import { createRootRoute, createRoute, Outlet, redirect } from "@tanstack/react-router";
2
+ import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
3
3
  import { CORE_UI_PLUGIN_KEY } from "./contract";
4
4
  import { digestOf } from "./descriptor";
5
5
  import type { PluginManifest, RouteRecord } from "./manifest-schema";
6
- import { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from "./mount-registry";
6
+ import { MOUNT_REGISTRY, type MountId, resolveMountSegment } from "./mount-registry";
7
7
  import { compareNavItems, type NavDeclaration, type NavItem, type NavManifest } from "./nav";
8
8
  import { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } from "./route-config";
9
9
 
@@ -11,9 +11,10 @@ import { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } fr
11
11
  * Host-side route-graph construction (ADR 0008 §1): manifests + route
12
12
  * configs in → host-built tree out. The host constructs ITS OWN Route
13
13
  * objects from validated manifest data via public `createRoute` APIs; no
14
- * foreign route object is ever mutated. Gates are attached HOST-side from
15
- * the mount registry — plugins cannot ship a route that escapes its
16
- * mount's gate because plugins never construct route objects.
14
+ * foreign route object is ever mutated. Mount guards are ordinary
15
+ * beforeLoads attached HOST-side from ConstructInput.guards — plugins
16
+ * cannot ship a route that escapes its mount's guard because plugins never
17
+ * construct route objects.
17
18
  *
18
19
  * Mounts are the ONLY cross-source parenting points: a root-level
19
20
  * pathless layout file (`_public.tsx`) declares the mount for the whole
@@ -34,57 +35,6 @@ function toAnyRoute(route: unknown): AnyRoute {
34
35
  return route as AnyRoute;
35
36
  }
36
37
 
37
- export interface GateUser {
38
- id: string;
39
- name?: string;
40
- /** Better Auth user role — "admin" gates the admin mount. */
41
- role?: string;
42
- }
43
-
44
- export interface HostContext {
45
- user?: GateUser;
46
- session?: { user?: GateUser };
47
- }
48
-
49
- type GateFn = (args: {
50
- context: HostContext;
51
- location: { pathname: string; searchStr: string };
52
- }) => void;
53
-
54
- /**
55
- * Gate redirects are absolute: gates attach to root-level mount routes, and
56
- * relatives would silently shift if a mount ever moved deeper in the tree.
57
- * The login surface is plugin-owned, never core-known.
58
- */
59
- const LOGIN_TO: string = "/login";
60
- const HOME_TO: string = "/";
61
-
62
- /**
63
- * Gate implementations by mount gate kind. Only `session` and `admin` are
64
- * implemented (ADR 0008 §3); unimplemented mounts are rejected during mount
65
- * declaration before any gate lookup, and gates are optional by type — a
66
- * mount without an entry (or `none`) renders ungated.
67
- */
68
- const GATES: Partial<Record<MountDef["gate"], GateFn>> = {
69
- session: ({ context, location }) => {
70
- const user = userOf(context);
71
- if (!user) {
72
- throw redirect({
73
- to: LOGIN_TO,
74
- search: { redirect: location.pathname + location.searchStr } as never,
75
- });
76
- }
77
- },
78
- admin: ({ context }) => {
79
- const user = userOf(context);
80
- if (!user) throw redirect({ to: LOGIN_TO });
81
- if (user.role !== "admin") throw redirect({ to: HOME_TO });
82
- },
83
- };
84
-
85
- const userOf = (context: HostContext | undefined): GateUser | undefined =>
86
- context?.user ?? context?.session?.user;
87
-
88
38
  export interface ConstructPluginRef {
89
39
  /** composition identity — plugin key; participates in the digest */
90
40
  key: string;
@@ -104,6 +54,8 @@ export interface ConstructInput {
104
54
  resolve: (ref: ConstructPluginRef) => Promise<ResolvedPlugin>;
105
55
  /** core ui `__root` options — component/error/notFound + head baseline */
106
56
  rootOptions?: RouteOptionsBundle;
57
+ /** Guard per mount id — beforeLoads supplied by the composing framework. */
58
+ guards?: Partial<Record<MountId, RouteOptionsBundle["beforeLoad"]>>;
107
59
  }
108
60
 
109
61
  export interface ConstructedTree {
@@ -139,17 +91,17 @@ const navOf = (options: RouteOptionsBundle): NavDeclaration | undefined => {
139
91
  };
140
92
 
141
93
  /** Layout routes (mount or pathless) pass their authored options through,
142
- * like every other route kind. On mount routes the registry gate composes
94
+ * like every other route kind. On mount routes the mount's guard composes
143
95
  * BEFORE the declaration's own beforeLoad so a source can never escape its
144
- * mount's gate (ADR 0008 §3), while the declaration's return value still
96
+ * mount's guard (ADR 0008 §3), while the declaration's return value still
145
97
  * flows to children as route context. */
146
98
  function layoutBeforeLoad(
147
- gate: GateFn | undefined,
99
+ guard: RouteOptionsBundle["beforeLoad"],
148
100
  declared: RouteOptionsBundle["beforeLoad"],
149
101
  ): RouteOptionsBundle["beforeLoad"] {
150
- if (!gate) return declared;
151
- return (args) => {
152
- gate(args);
102
+ if (!guard) return declared;
103
+ return async (args) => {
104
+ await guard(args);
153
105
  return declared?.(args);
154
106
  };
155
107
  }
@@ -270,13 +222,16 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
270
222
  const declaration = declarations.get(mountId);
271
223
  if (!declaration) continue;
272
224
  const def = MOUNT_REGISTRY[mountId]!;
273
- const gate = GATES[def.gate];
225
+ const guard = input.guards?.[mountId];
226
+ if (def.guarded && !guard) {
227
+ throw new Error(`mount "${mountId}" requires a guard — add one to ConstructInput.guards`);
228
+ }
274
229
  const options = declaration.options;
275
230
  const route = createRoute({
276
231
  id: `__mount_${mountId}`,
277
232
  getParentRoute: () => rootRoute,
278
- ...(gate || options.beforeLoad
279
- ? { beforeLoad: layoutBeforeLoad(gate, options.beforeLoad) }
233
+ ...(guard || options.beforeLoad
234
+ ? { beforeLoad: layoutBeforeLoad(guard, options.beforeLoad) }
280
235
  : {}),
281
236
  ...authoredOptions(options),
282
237
  component: options.component ?? Outlet,
@@ -10,8 +10,6 @@ export {
10
10
  type ConstructInput,
11
11
  type ConstructPluginRef,
12
12
  constructTree,
13
- type GateUser,
14
- type HostContext,
15
13
  type ResolvedPlugin,
16
14
  } from "./construct";
17
15
  export * from "./contract";
@@ -1,40 +1,40 @@
1
1
  /**
2
- * Mount registry v2 — gates only, shape-free (ADR 0008 §3).
2
+ * Mount registry v2 — shape-free (ADR 0008 §3).
3
3
  *
4
- * A mount is a gate + a host-owned pathless layout position. It carries no
5
- * product semantics (no "dashboard", no shell, no chrome) — plugins bring
6
- * their own layouts and route freely inside their subtree. A mount is
7
- * declared by a ROOT-level pathless layout file whose `_`-prefixed segment
8
- * names the mount (`_public.tsx` → `public`); the generator derives it and
9
- * validates it against this registry; gates are attached HOST-side at
10
- * construction, so a plugin cannot ship a route that escapes its mount's
11
- * gate (plugins never construct route objects).
4
+ * A mount is a host-owned pathless layout position. It carries no product
5
+ * semantics (no "dashboard", no shell, no chrome) — plugins bring their own
6
+ * layouts and route freely inside their subtree. A mount is declared by a
7
+ * ROOT-level pathless layout file whose `_`-prefixed segment names the mount
8
+ * (`_public.tsx` → `public`); the generator derives it and validates it
9
+ * against this registry; guards are ordinary beforeLoads attached HOST-side
10
+ * at construction from ConstructInput.guards, so a plugin cannot ship a
11
+ * route that escapes its mount's guard (plugins never construct route
12
+ * objects).
12
13
  *
13
- * `org`/`team` are declared vocabulary (parameterized membership gates) but
14
+ * `org`/`team` are declared vocabulary (parameterized membership) but
14
15
  * unimplemented — construction rejects them until they land.
15
16
  */
16
17
 
17
18
  export interface MountDef {
18
- label: string;
19
- gate: "none" | "session" | "admin" | "organization" | "team";
20
- parameterized?: boolean;
19
+ /** The composing framework supplies this mount's guard via ConstructInput.guards. */
20
+ guarded: boolean;
21
21
  implemented: boolean;
22
22
  }
23
23
 
24
24
  export const MOUNT_REGISTRY = {
25
- public: { label: "public", gate: "none", implemented: true },
26
- authenticated: { label: "authenticated", gate: "session", implemented: true },
27
- admin: { label: "admin", gate: "admin", implemented: true },
28
- org: { label: "org", gate: "organization", parameterized: true, implemented: false },
29
- team: { label: "team", gate: "team", parameterized: true, implemented: false },
30
- } satisfies Record<string, MountDef>;
25
+ public: { guarded: false, implemented: true },
26
+ authenticated: { guarded: true, implemented: true },
27
+ admin: { guarded: true, implemented: true },
28
+ org: { guarded: true, implemented: false },
29
+ team: { guarded: true, implemented: false },
30
+ } as const satisfies Record<string, MountDef>;
31
31
 
32
32
  export type MountId = Extract<keyof typeof MOUNT_REGISTRY, string>;
33
33
 
34
34
  export const MOUNTS = Object.keys(MOUNT_REGISTRY) as MountId[];
35
35
 
36
36
  /** Bump when the registry shape changes — invalidates all compose digests. */
37
- export const MOUNT_REGISTRY_VERSION = 5;
37
+ export const MOUNT_REGISTRY_VERSION = 6;
38
38
 
39
39
  /** Migration aliases: legacy `_`-segment names → canonical mounts. */
40
40
  export const MOUNT_ALIASES: Record<string, MountId> = {
@@ -12,10 +12,24 @@ import { z } from "zod";
12
12
 
13
13
  const SRI = z.string().regex(/^sha384-[A-Za-z0-9+/=]+$/);
14
14
 
15
+ /** Slot-relative directory holding one deploy's immutable file copies (#826). */
16
+ export const VERSION_DIR_PATTERN = /^versions\/[0-9a-f]{16}$/;
17
+
15
18
  export const WorkspaceVersionManifestSchema = z.object({
16
19
  version: z.string().regex(/^[0-9a-f]{16}$/),
17
20
  builtAt: z.string().optional(),
18
21
  gitSha: z.string().optional(),
22
+ /**
23
+ * Slot-relative directory (`versions/<dirId>`) holding this deploy's
24
+ * byte-immutable copies of every dist file (#826). When present, all
25
+ * artifact coordinates below resolve against `<slotBase>/<dir>/` — so a
26
+ * pinned child's entry fetches its expose chunk and chunks from the same
27
+ * frozen directory instead of the mutable slot root. Absent on legacy
28
+ * manifests: coordinates resolve against the slot root exactly as before.
29
+ * Derived from the `files` map (see `versionDirPath`), so it participates
30
+ * in the version hash without circularity.
31
+ */
32
+ dir: z.string().regex(VERSION_DIR_PATTERN).optional(),
19
33
  entry: z.string().min(1),
20
34
  entryIntegrity: SRI,
21
35
  ssr: z
@@ -67,6 +81,7 @@ export interface VersionManifestInput {
67
81
  * staged copy and the CDN's immutability for republished versions. */
68
82
  builtAt?: string;
69
83
  gitSha?: string;
84
+ dir?: string;
70
85
  entry: string;
71
86
  entryIntegrity: string;
72
87
  ssr?: { entry: string; integrity: string };
@@ -93,6 +108,18 @@ export function composeVersionManifest(input: VersionManifestInput): WorkspaceVe
93
108
  return WorkspaceVersionManifestSchema.parse({ ...input, version });
94
109
  }
95
110
 
111
+ /**
112
+ * The content-addressed directory name for one dist's file set (#826): a
113
+ * 16-hex hash over the canonical files map. Identical bytes → identical
114
+ * directory → re-deploying unchanged content is a server-side no-op, and a
115
+ * published pin's directory can never be rewritten by a later deploy
116
+ * (the upload service refuses conflicting writes under `versions/`).
117
+ */
118
+ export function versionDirPath(files: Record<string, string>): string {
119
+ const id = createHash("sha256").update(stableStringify(files)).digest("hex").slice(0, 16);
120
+ return `versions/${id}`;
121
+ }
122
+
96
123
  function stableStringify(value: unknown): string {
97
124
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
98
125
  if (value && typeof value === "object") {