@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
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/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Fixture and result types for the realm.ts test harness.
|
|
2
|
+
//
|
|
3
|
+
// `ConfigOf<App>` walks the app's configSchema and emits the shape an
|
|
4
|
+
// author writes in tests: every input leaf, with secrets typed as plain
|
|
5
|
+
// `string` (the harness wraps them into ref markers before the sandbox
|
|
6
|
+
// runs). Permission leaves are excluded — those go in `grants`.
|
|
7
|
+
//
|
|
8
|
+
// `GrantsOf<App>` mirrors the runtime `Grants` shape: a flat record keyed
|
|
9
|
+
// by the schema paths that declared a permission, valued `{kind, data}` of
|
|
10
|
+
// that permission's kind. `r.X().optional()` permissions become optional
|
|
11
|
+
// keys in the record.
|
|
12
|
+
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
import type { CompileIssue } from "../errors.ts";
|
|
15
|
+
import type { CompleteInput, CompletePermission } from "../factory.ts";
|
|
16
|
+
import type { AppDefinition, PreviewContext } from "../create-app.ts";
|
|
17
|
+
import type { ConfigSchema } from "../types.ts";
|
|
18
|
+
import type { TabRecord, SectionRecord } from "../schema/nodes.ts";
|
|
19
|
+
import type { HomeApp } from "../resolve.ts";
|
|
20
|
+
import type { ResolvedInfoEntry } from "../info.ts";
|
|
21
|
+
import type { GrantOf } from "../grants.ts";
|
|
22
|
+
import type { PublicDomainGrantData } from "../schema/public-domain.ts";
|
|
23
|
+
import type { WorldState } from "../peer-world.ts";
|
|
24
|
+
import type { Violation } from "../security.ts";
|
|
25
|
+
|
|
26
|
+
type FixtureInputValueOf<I> = I extends { readonly kind: "input.text" }
|
|
27
|
+
? string
|
|
28
|
+
: I extends { readonly kind: "input.secret" }
|
|
29
|
+
? string
|
|
30
|
+
: I extends { readonly kind: "input.number" }
|
|
31
|
+
? number
|
|
32
|
+
: I extends { readonly kind: "input.boolean" }
|
|
33
|
+
? boolean
|
|
34
|
+
: I extends { readonly kind: "input.enum"; readonly schema: z.ZodType<infer V> }
|
|
35
|
+
? V
|
|
36
|
+
: never;
|
|
37
|
+
|
|
38
|
+
type IsOptional<N> = N extends { readonly meta: { readonly isOptional: true } } ? true : false;
|
|
39
|
+
|
|
40
|
+
type FixtureValueOf<N> = N extends CompletePermission
|
|
41
|
+
? never
|
|
42
|
+
: N extends CompleteInput
|
|
43
|
+
? FixtureInputValueOf<N>
|
|
44
|
+
: N extends TabRecord<infer C>
|
|
45
|
+
? FixtureTree<C>
|
|
46
|
+
: N extends SectionRecord<infer C>
|
|
47
|
+
? FixtureTree<C>
|
|
48
|
+
: never;
|
|
49
|
+
|
|
50
|
+
type IsPermission<N> = N extends CompletePermission ? true : false;
|
|
51
|
+
|
|
52
|
+
type FixtureTree<C> = {
|
|
53
|
+
readonly [K in keyof C as IsPermission<C[K]> extends true
|
|
54
|
+
? never
|
|
55
|
+
: IsOptional<C[K]> extends true
|
|
56
|
+
? never
|
|
57
|
+
: K]: FixtureValueOf<C[K]>;
|
|
58
|
+
} & {
|
|
59
|
+
readonly [K in keyof C as IsPermission<C[K]> extends true
|
|
60
|
+
? never
|
|
61
|
+
: IsOptional<C[K]> extends true
|
|
62
|
+
? K
|
|
63
|
+
: never]?: FixtureValueOf<C[K]>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ConfigOf<App> =
|
|
67
|
+
App extends AppDefinition<infer S extends ConfigSchema> ? FixtureTree<S> : never;
|
|
68
|
+
|
|
69
|
+
// ----- GrantsOf -----------------------------------------------------------
|
|
70
|
+
//
|
|
71
|
+
// Walks the schema and collects every CompletePermission leaf, keyed by
|
|
72
|
+
// dotted path. Each value is `GrantOf<Leaf>` — `kind` narrowed to that
|
|
73
|
+
// permission's literal kind, `data` to the kind's declared data shape.
|
|
74
|
+
|
|
75
|
+
type PermissionPaths<S, Prefix extends string = ""> =
|
|
76
|
+
S extends Record<string, unknown>
|
|
77
|
+
? {
|
|
78
|
+
[K in keyof S & string]: S[K] extends CompletePermission
|
|
79
|
+
? `${Prefix}${K}`
|
|
80
|
+
: S[K] extends TabRecord<infer C>
|
|
81
|
+
? PermissionPaths<C, `${Prefix}${K}.`>
|
|
82
|
+
: S[K] extends SectionRecord<infer C>
|
|
83
|
+
? PermissionPaths<C, `${Prefix}${K}.`>
|
|
84
|
+
: never;
|
|
85
|
+
}[keyof S & string]
|
|
86
|
+
: never;
|
|
87
|
+
|
|
88
|
+
type PermissionAtPath<S, P extends string> = P extends `${infer Head}.${infer Rest}`
|
|
89
|
+
? Head extends keyof S
|
|
90
|
+
? S[Head] extends TabRecord<infer C>
|
|
91
|
+
? PermissionAtPath<C, Rest>
|
|
92
|
+
: S[Head] extends SectionRecord<infer C>
|
|
93
|
+
? PermissionAtPath<C, Rest>
|
|
94
|
+
: never
|
|
95
|
+
: never
|
|
96
|
+
: P extends keyof S
|
|
97
|
+
? S[P] extends CompletePermission
|
|
98
|
+
? S[P]
|
|
99
|
+
: never
|
|
100
|
+
: never;
|
|
101
|
+
|
|
102
|
+
type RequiredPermissionPaths<S> = {
|
|
103
|
+
[P in PermissionPaths<S>]: PermissionAtPath<S, P> extends infer Leaf
|
|
104
|
+
? Leaf extends CompletePermission
|
|
105
|
+
? IsOptional<Leaf> extends true
|
|
106
|
+
? never
|
|
107
|
+
: P
|
|
108
|
+
: never
|
|
109
|
+
: never;
|
|
110
|
+
}[PermissionPaths<S>];
|
|
111
|
+
|
|
112
|
+
type OptionalPermissionPaths<S> = {
|
|
113
|
+
[P in PermissionPaths<S>]: PermissionAtPath<S, P> extends infer Leaf
|
|
114
|
+
? Leaf extends CompletePermission
|
|
115
|
+
? IsOptional<Leaf> extends true
|
|
116
|
+
? P
|
|
117
|
+
: never
|
|
118
|
+
: never
|
|
119
|
+
: never;
|
|
120
|
+
}[PermissionPaths<S>];
|
|
121
|
+
|
|
122
|
+
// The stored-grant shape for a leaf. Identical to `GrantOf` for every kind
|
|
123
|
+
// whose descriptor data and grant data are the same shape. Two diverge, both
|
|
124
|
+
// because the operator supplies the elevated value, so the descriptor carries
|
|
125
|
+
// only the author's ask:
|
|
126
|
+
//
|
|
127
|
+
// - `host.silo`: the record's data is the author's asked flags (they drive
|
|
128
|
+
// the `config.silo` narrowing), the stored grant is the Silo's id.
|
|
129
|
+
// - `home.publicDomain`: the record's data is `true`, the stored grant is
|
|
130
|
+
// the operator's hostnames plus their redirect choice.
|
|
131
|
+
//
|
|
132
|
+
// Without these branches a fixture's `grants` entry types as `data: true`.
|
|
133
|
+
type StoredGrantOf<P> =
|
|
134
|
+
GrantOf<P> extends { readonly kind: "host.silo" }
|
|
135
|
+
? { readonly kind: "host.silo"; readonly data: string }
|
|
136
|
+
: GrantOf<P> extends { readonly kind: "home.publicDomain" }
|
|
137
|
+
? { readonly kind: "home.publicDomain"; readonly data: PublicDomainGrantData }
|
|
138
|
+
: GrantOf<P>;
|
|
139
|
+
|
|
140
|
+
type FlatGrants<S> = {
|
|
141
|
+
readonly [P in RequiredPermissionPaths<S>]: StoredGrantOf<PermissionAtPath<S, P>>;
|
|
142
|
+
} & {
|
|
143
|
+
readonly [P in OptionalPermissionPaths<S>]?: StoredGrantOf<PermissionAtPath<S, P>>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type GrantsOf<App> =
|
|
147
|
+
App extends AppDefinition<infer S extends ConfigSchema> ? FlatGrants<S> : never;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One Silo as a compile-tier fixture describes it - the stand-in for the row
|
|
151
|
+
* the operator would have created. Keyed by Silo id in `CompileFixture.silo`,
|
|
152
|
+
* so the id itself isn't a field here.
|
|
153
|
+
*/
|
|
154
|
+
export interface SiloFixture {
|
|
155
|
+
/** The shared directory. Required when the ask includes `volume`. */
|
|
156
|
+
hostPath?: string;
|
|
157
|
+
/** Where members mount it. Defaults to `/silo`, as a real Silo does. */
|
|
158
|
+
containerPath?: string;
|
|
159
|
+
/** The Silo's group. Required when the ask includes `volume`. */
|
|
160
|
+
gid?: number;
|
|
161
|
+
/**
|
|
162
|
+
* What this Silo's other members publish (name → port) - the directory a
|
|
163
|
+
* require-map resolves against. Defaults to `{}`, so any require-map without
|
|
164
|
+
* it fails with the same guided message Home emits. Per Silo: a require
|
|
165
|
+
* against one Silo never sees another's publications, same as Home.
|
|
166
|
+
*/
|
|
167
|
+
services?: Record<string, number>;
|
|
168
|
+
/**
|
|
169
|
+
* The shared credential. Defaults to a stable 64-hex string derived from
|
|
170
|
+
* `id` - production *shape* without needing REALM_SECRET.
|
|
171
|
+
*/
|
|
172
|
+
apiKey?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface CompileFixture<App> {
|
|
176
|
+
config: ConfigOf<App>;
|
|
177
|
+
grants?: GrantsOf<App>;
|
|
178
|
+
/** Optional override; defaults to the app's `name`. */
|
|
179
|
+
appId?: string;
|
|
180
|
+
/**
|
|
181
|
+
* HMAC seed used to derive per-app passwords. Defaults to `"test"` so
|
|
182
|
+
* compiles are deterministic across runs - two `t.compile(...)` calls
|
|
183
|
+
* with the same fixture produce the same `derived` secrets, which lets
|
|
184
|
+
* tests assert on resolved env values directly.
|
|
185
|
+
*/
|
|
186
|
+
seed?: string;
|
|
187
|
+
/**
|
|
188
|
+
* Peer-world entries to expose to this app's `peers.*` permissions.
|
|
189
|
+
* Each entry is `{kind, data}` for one of the registered peer kinds
|
|
190
|
+
* (routedApps, hostPorts, backupVolumes, buildApps). Defaults to
|
|
191
|
+
* `[]`. A `peers.*` permission still requires a matching grant - same
|
|
192
|
+
* as Home - so unset grants resolve to `[]` regardless of `peers`.
|
|
193
|
+
*/
|
|
194
|
+
peers?: WorldState;
|
|
195
|
+
/**
|
|
196
|
+
* Override for the bundled `host.management()` resolved value. At
|
|
197
|
+
* runtime Home fills `baseDomain` / `homeSlug` from `home_state` and
|
|
198
|
+
* `workerId` from the running worker; tests supply whatever subset
|
|
199
|
+
* they need. Fields left unset stay as the SDK placeholder
|
|
200
|
+
* (empty string), matching a fresh install before configuration.
|
|
201
|
+
* Apps that don't grant `host.management()` ignore this entirely.
|
|
202
|
+
*/
|
|
203
|
+
management?: {
|
|
204
|
+
baseDomain?: string;
|
|
205
|
+
homeSlug?: string;
|
|
206
|
+
workerId?: string;
|
|
207
|
+
acmeDns?: {
|
|
208
|
+
serverUrl: string;
|
|
209
|
+
username: string;
|
|
210
|
+
apiKey: string;
|
|
211
|
+
ca: string;
|
|
212
|
+
caRootPem: string;
|
|
213
|
+
} | null;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Override for the bundled `home.apiAccess()` resolved value. At runtime
|
|
217
|
+
* Home fills `socket` with the app's per-app proxy DIRECTORY
|
|
218
|
+
* (`/opt/realm/worker/home-proxy/<appId>`, the dir the app mounts); tests
|
|
219
|
+
* supply whatever path they want to mount/assert on. The supplied path is
|
|
220
|
+
* also exempted from the absolute-mount gate (mirrors Home's
|
|
221
|
+
* `collectAllowed`). Apps that don't grant `home.apiAccess()` ignore this
|
|
222
|
+
* entirely.
|
|
223
|
+
*/
|
|
224
|
+
homeApi?: {
|
|
225
|
+
socket: string;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Override for the bundled `host.spawn()` resolved value. At runtime Home
|
|
229
|
+
* fills `socket` + `scratch` + `artifacts` with the app's per-app paths
|
|
230
|
+
* (`/opt/realm/worker/spawn/sock/<appId>` — the socket DIRECTORY the app
|
|
231
|
+
* mounts — + `data/<appId>/scratch` + `data/<appId>/artifacts`); tests supply
|
|
232
|
+
* whatever paths they want to mount/assert on. All three supplied paths are
|
|
233
|
+
* exempted from the absolute-mount gate (mirrors Home's
|
|
234
|
+
* `collectAllowed`). Apps that don't grant `host.spawn()` ignore this
|
|
235
|
+
* entirely.
|
|
236
|
+
*/
|
|
237
|
+
spawn?: {
|
|
238
|
+
socket: string;
|
|
239
|
+
scratch: string;
|
|
240
|
+
artifacts: string;
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* The Silo (or Silos) a granted `host.silo()` resolves against. At runtime
|
|
244
|
+
* these are rows the operator created and bound the app to; there's no such
|
|
245
|
+
* record in a test, so the fixture supplies them. The declared affordances
|
|
246
|
+
* still come from the app's own `r.host.silo({...})` - this only says which
|
|
247
|
+
* Silo, the same split as the real install (the author asks, the operator
|
|
248
|
+
* points).
|
|
249
|
+
*
|
|
250
|
+
* Keyed by Silo id - the same id the grant carries, so a `host.silo` grant of
|
|
251
|
+
* `"k3f9x2"` resolves against the `k3f9x2` entry:
|
|
252
|
+
*
|
|
253
|
+
* ```ts
|
|
254
|
+
* grants: { silo: { kind: "host.silo", data: "k3f9x2" } },
|
|
255
|
+
* silo: { k3f9x2: { hostPath: "/mnt/media", gid: 1_000_000 } },
|
|
256
|
+
* ```
|
|
257
|
+
*
|
|
258
|
+
* An app binding two Silos gets two entries, and each declaration resolves
|
|
259
|
+
* against its own directory and its own `services` - the same separation Home
|
|
260
|
+
* gets from two rows.
|
|
261
|
+
*
|
|
262
|
+
* Only the fields the ask actually uses are needed: `hostPath` + `gid` when
|
|
263
|
+
* the ask includes `volume`, `services` when it carries a require-map. The
|
|
264
|
+
* volume's `hostPath` is exempted from the absolute-mount gate, and
|
|
265
|
+
* the (hostPath, gid) pair is what the mount gate matches against - so a
|
|
266
|
+
* fixture that hand-writes a different gid gets the same violation a real
|
|
267
|
+
* app would. Apps that don't grant `host.silo()` ignore this entirely.
|
|
268
|
+
*/
|
|
269
|
+
silo?: Record<string, SiloFixture>;
|
|
270
|
+
/**
|
|
271
|
+
* Per-install preview context handed to `getDesiredState` as `preview`.
|
|
272
|
+
* Defaults to `null` (a persistent install). Set `{ previewKey }` to compile
|
|
273
|
+
* the app as a template's preview child and assert on its preview-branched
|
|
274
|
+
* state (e.g. a per-PR public domain).
|
|
275
|
+
*/
|
|
276
|
+
preview?: PreviewContext | null;
|
|
277
|
+
/**
|
|
278
|
+
* Home's mesh base domain, used to resolve `info`'s `services.<name>` URLs
|
|
279
|
+
* (`{appId}-{service}.{slug}.{baseDomain}`). Defaults to unset, so service
|
|
280
|
+
* URLs resolve to `null` (an unplaced install) and `info` entries that lean
|
|
281
|
+
* on them drop. Mirrors Home's `home_state` baseDomain.
|
|
282
|
+
*/
|
|
283
|
+
baseDomain?: string;
|
|
284
|
+
/**
|
|
285
|
+
* Placement worker slug, the other half of the structured service host used
|
|
286
|
+
* to resolve `info`'s `services.<name>` URLs. Defaults to unset (`null`).
|
|
287
|
+
*/
|
|
288
|
+
slug?: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export type CompileResult =
|
|
292
|
+
| {
|
|
293
|
+
error: false;
|
|
294
|
+
app: HomeApp;
|
|
295
|
+
/**
|
|
296
|
+
* The app's `info` entries, resolved on the same separate path Home's
|
|
297
|
+
* summary page uses. A sibling of `app` (not folded into it) because
|
|
298
|
+
* `info` is not part of the resolved worker-bound state - keeping it
|
|
299
|
+
* off `app` leaves resolved-state snapshots untouched.
|
|
300
|
+
*/
|
|
301
|
+
info: ResolvedInfoEntry[];
|
|
302
|
+
}
|
|
303
|
+
| {
|
|
304
|
+
error: true;
|
|
305
|
+
message: string;
|
|
306
|
+
issues: readonly CompileIssue[];
|
|
307
|
+
/**
|
|
308
|
+
* Raw violation records from `checkAppGrants` for the subset of
|
|
309
|
+
* issues that came from grant checking. Carries the structured
|
|
310
|
+
* shape Home returns from POST /api/apps so the dev install panel
|
|
311
|
+
* can render the per-permission violations list rather than a
|
|
312
|
+
* flat error message. Empty for non-grant failures (eval throws,
|
|
313
|
+
* bundle errors, validation, etc).
|
|
314
|
+
*/
|
|
315
|
+
violations: readonly Violation[];
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export type { CompileIssue };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Resolving a sampled `getTrustedHeaders` manifest against a live viewer.
|
|
2
|
+
//
|
|
3
|
+
// The manifest is static (header names must be knowable without a request,
|
|
4
|
+
// so Caddy can list them for copy_headers / anti-spoof strip); the values
|
|
5
|
+
// are per-request. This module is the single place that turns one into the
|
|
6
|
+
// other, for both doors: the ForwardAuth verify hop (`access-gate.ts`, the
|
|
7
|
+
// browser reaching an authenticated subdomain) and the UI surface hop
|
|
8
|
+
// (`run-loader.ts`, a page or widget loader/action going iframe → Home →
|
|
9
|
+
// worker → container).
|
|
10
|
+
//
|
|
11
|
+
// Plain data in, plain data out — no `Deno`, no Web globals.
|
|
12
|
+
|
|
13
|
+
import type { TrustedHeaderSource, TrustedHeadersManifest } from "./resolve.ts";
|
|
14
|
+
|
|
15
|
+
type ByRole = TrustedHeadersManifest["byRole"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The live Home user a sampled mapping resolves against. Structurally the
|
|
19
|
+
* runtime half of `User` in create-app.ts, where `username` / `email` are
|
|
20
|
+
* refs because there is no real user at sampling time.
|
|
21
|
+
*/
|
|
22
|
+
export interface TrustedHeaderViewer {
|
|
23
|
+
username: string;
|
|
24
|
+
email: string;
|
|
25
|
+
role: "admin" | "member";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Apply an app's sampled per-role mapping to the viewer. Returns an empty
|
|
30
|
+
* map when the app brokers no headers (no manifest) or declares none for
|
|
31
|
+
* this viewer's role.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveTrustedHeaders(
|
|
34
|
+
byRole: ByRole | undefined,
|
|
35
|
+
viewer: TrustedHeaderViewer,
|
|
36
|
+
): Record<string, string> {
|
|
37
|
+
const map = byRole?.[viewer.role];
|
|
38
|
+
if (!map) return {};
|
|
39
|
+
const out: Record<string, string> = {};
|
|
40
|
+
for (const [key, source] of Object.entries(map)) {
|
|
41
|
+
out[key] = resolveSource(source, viewer);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve a header source against the live viewer: a literal passes through,
|
|
47
|
+
// a `{field}` reads the session value, a `{template}` joins literal + field
|
|
48
|
+
// segments in order.
|
|
49
|
+
function resolveSource(source: TrustedHeaderSource, viewer: TrustedHeaderViewer): string {
|
|
50
|
+
if ("literal" in source) return source.literal;
|
|
51
|
+
if ("field" in source) return viewer[source.field];
|
|
52
|
+
return source.template.map((seg) => (typeof seg === "string" ? seg : viewer[seg.field])).join("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Layer Home-stamped headers over the ones the author declared on a
|
|
57
|
+
* loader/action call, dropping any declared key that collides with a
|
|
58
|
+
* stamped name in *any* casing.
|
|
59
|
+
*
|
|
60
|
+
* The case-insensitivity is the point. A plain spread only overrides an
|
|
61
|
+
* exact-case duplicate, so an author-declared `x-realm-widget-pid` survives
|
|
62
|
+
* beside Home's `X-Realm-Widget-PID` as a second entry and both reach the
|
|
63
|
+
* container comma-joined — the app then reads a value it authored itself.
|
|
64
|
+
* Returns `undefined` when there is nothing to send.
|
|
65
|
+
*/
|
|
66
|
+
export function stampHeaders(
|
|
67
|
+
declared: Record<string, string> | undefined,
|
|
68
|
+
stamped: Record<string, string>,
|
|
69
|
+
): Record<string, string> | undefined {
|
|
70
|
+
const stampedKeys = Object.keys(stamped);
|
|
71
|
+
if (stampedKeys.length === 0) return declared;
|
|
72
|
+
const reserved = new Set(stampedKeys.map((k) => k.toLowerCase()));
|
|
73
|
+
const out: Record<string, string> = {};
|
|
74
|
+
for (const [k, v] of Object.entries(declared ?? {})) {
|
|
75
|
+
if (reserved.has(k.toLowerCase())) continue;
|
|
76
|
+
out[k] = v;
|
|
77
|
+
}
|
|
78
|
+
return { ...out, ...stamped };
|
|
79
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Type-level wiring for createApp's `getDesiredState` parameter.
|
|
2
|
+
//
|
|
3
|
+
// A configSchema is a tree of typed records (permissions, inputs, sections,
|
|
4
|
+
// tabs). At eval time it collapses into a `config` object whose shape
|
|
5
|
+
// depends on each node:
|
|
6
|
+
//
|
|
7
|
+
// r.host.port(9090).reason(...) → { port: number }
|
|
8
|
+
// r.input.text() → string
|
|
9
|
+
// r.input.secret() → Ref<string> (resolved at deploy time)
|
|
10
|
+
// r.section({ a: r.input.text() }) → { a: string }
|
|
11
|
+
// r.tab({ s: r.section(...) }) → { s: { ... } }
|
|
12
|
+
//
|
|
13
|
+
// Optional nodes flip from required keys to optional keys in the resolved
|
|
14
|
+
// tree.
|
|
15
|
+
|
|
16
|
+
import type { z } from "zod";
|
|
17
|
+
import type { Ref } from "./ref.ts";
|
|
18
|
+
import type { CompleteInput, CompletePermission, CompleteSchemablePermission } from "./factory.ts";
|
|
19
|
+
import type { Grant, GrantOf, ValueOf } from "./grants.ts";
|
|
20
|
+
import type { TabChild, TabRecord, SectionChild, SectionRecord } from "./schema/nodes.ts";
|
|
21
|
+
import type { InputKind } from "./shared.ts";
|
|
22
|
+
|
|
23
|
+
// ----- Node hierarchy -----------------------------------------------------
|
|
24
|
+
|
|
25
|
+
// CompleteSchemablePermission is the post-`.schema()` shape of a fetchUrl
|
|
26
|
+
// factory; it lands here so r.remote.fetchUrl(...).schema(z).reason("...")
|
|
27
|
+
// is assignable to a configSchema slot. The pre-`.schema()` factory
|
|
28
|
+
// output is missing `responseSchema`, which CompleteSchemablePermission
|
|
29
|
+
// requires — so an unschema'd fetchUrl fails to typecheck in configSchema.
|
|
30
|
+
export type Leaf = CompletePermission | CompleteSchemablePermission | CompleteInput;
|
|
31
|
+
|
|
32
|
+
export type Section = SectionRecord<Record<string, SectionChild>>;
|
|
33
|
+
export type Tab = TabRecord<Record<string, TabChild>>;
|
|
34
|
+
|
|
35
|
+
export type Node = Leaf | Section | Tab;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A configSchema is either a flat record of leaves and sections, or a
|
|
39
|
+
* record of tabs. `createApp`'s homogeneity check enforces "all tabs or
|
|
40
|
+
* no tabs" at runtime; sections fall on the no-tabs side and can sit
|
|
41
|
+
* alongside leaves at the top level.
|
|
42
|
+
*/
|
|
43
|
+
export type ConfigSchema = Record<string, Leaf | Section> | Record<string, Tab>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A ui surface's `configSchema` — the per-mount settings a user fills in on
|
|
47
|
+
* the tile's config screen. Narrower than an app's `ConfigSchema` on purpose:
|
|
48
|
+
*
|
|
49
|
+
* - **Inputs only, no permissions.** Permissions are app-level grants, not
|
|
50
|
+
* per-user per-placement settings; they have no meaning on a widget tile.
|
|
51
|
+
* - **Flat, no tabs/sections.** A tile's settings are a short list, not a
|
|
52
|
+
* multi-page form.
|
|
53
|
+
* - **No `r.input.secret()`.** A secret leaf can't ride the in-iframe
|
|
54
|
+
* `request(state, config)` (the iframe never sees it), so it needs a
|
|
55
|
+
* server-side delivery channel that doesn't exist yet. Excluding
|
|
56
|
+
* `input.secret` at the type level makes a secret leaf a compile error
|
|
57
|
+
* instead of one that would silently ship to the iframe. (`create-app`'s
|
|
58
|
+
* manifest builder also throws on one, for the packed/JS path that skips
|
|
59
|
+
* the type check.)
|
|
60
|
+
*/
|
|
61
|
+
export type NonSecretInput = CompleteInput & {
|
|
62
|
+
readonly kind: Exclude<InputKind, "input.secret">;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type SurfaceConfigSchema = Record<string, NonSecretInput>;
|
|
66
|
+
|
|
67
|
+
// ----- Per-input runtime value --------------------------------------------
|
|
68
|
+
|
|
69
|
+
type InputValueOf<I> = I extends { readonly kind: "input.text" }
|
|
70
|
+
? string
|
|
71
|
+
: I extends { readonly kind: "input.secret" }
|
|
72
|
+
? Ref<string>
|
|
73
|
+
: I extends { readonly kind: "input.number" }
|
|
74
|
+
? number
|
|
75
|
+
: I extends { readonly kind: "input.boolean" }
|
|
76
|
+
? boolean
|
|
77
|
+
: I extends { readonly kind: "input.enum"; readonly schema: z.ZodType<infer V> }
|
|
78
|
+
? V
|
|
79
|
+
: never;
|
|
80
|
+
|
|
81
|
+
// ----- ResolvedTree -------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
type IsOptionalNode<N> = N extends { readonly meta: { readonly isOptional: true } } ? true : false;
|
|
84
|
+
|
|
85
|
+
type ResolvedNode<N> =
|
|
86
|
+
// CompleteSchemablePermission (fetchUrl after .schema()) is checked
|
|
87
|
+
// before CompletePermission because it carries a live schema that lets
|
|
88
|
+
// the resolved value type narrow to the schema's output — authors get
|
|
89
|
+
// typed access to the validated JSON.
|
|
90
|
+
N extends CompleteSchemablePermission
|
|
91
|
+
? N extends { readonly responseSchema: z.ZodType<infer V> }
|
|
92
|
+
? V
|
|
93
|
+
: unknown
|
|
94
|
+
: N extends CompletePermission
|
|
95
|
+
? // toGrant strips meta to {kind, data}; ValueOf maps that to the runtime shape.
|
|
96
|
+
ValueOf<Extract<GrantOf<N>, Grant>>
|
|
97
|
+
: N extends CompleteInput
|
|
98
|
+
? InputValueOf<N>
|
|
99
|
+
: N extends TabRecord<infer C>
|
|
100
|
+
? ResolvedTree<C>
|
|
101
|
+
: N extends SectionRecord<infer C>
|
|
102
|
+
? ResolvedTree<C>
|
|
103
|
+
: never;
|
|
104
|
+
|
|
105
|
+
export type ResolvedTree<C> = {
|
|
106
|
+
readonly [K in keyof C as IsOptionalNode<C[K]> extends true ? never : K]: ResolvedNode<C[K]>;
|
|
107
|
+
} & {
|
|
108
|
+
readonly [K in keyof C as IsOptionalNode<C[K]> extends true ? K : never]?: ResolvedNode<C[K]>;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type InferConfigSchema<T> =
|
|
112
|
+
T extends Record<string, Node> ? ResolvedTree<T> : Record<string, never>;
|
|
113
|
+
|
|
114
|
+
// ----- Runtime narrowers --------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export function isTab(node: Node): node is Tab {
|
|
117
|
+
return node.kind === "node.tab";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isSection(node: Node): node is Section {
|
|
121
|
+
return node.kind === "node.section";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isInput(node: Node): node is CompleteInput {
|
|
125
|
+
return typeof node.kind === "string" && node.kind.startsWith("input.");
|
|
126
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
import { cn } from "../utils.ts";
|
|
3
|
+
import {
|
|
4
|
+
InfoIcon,
|
|
5
|
+
LightbulbIcon,
|
|
6
|
+
RocketLaunchIcon,
|
|
7
|
+
WarningIcon,
|
|
8
|
+
WarningOctagonIcon,
|
|
9
|
+
} from "./icons.tsx";
|
|
10
|
+
|
|
11
|
+
// The docs' callout anatomy - a coloured clay panel with a 24px icon, an
|
|
12
|
+
// uppercase title and one short body - available to the dashboard. The Updates
|
|
13
|
+
// page uses it for the consequence statement above the primary button, which
|
|
14
|
+
// is why the spacing is the caller's: it sits inside a flex column there and
|
|
15
|
+
// under a paragraph in the docs.
|
|
16
|
+
|
|
17
|
+
type AsideVariant = "note" | "tip" | "caution" | "danger" | "roadmap";
|
|
18
|
+
|
|
19
|
+
interface VariantStyle {
|
|
20
|
+
bg: string;
|
|
21
|
+
shadow: string;
|
|
22
|
+
text: string;
|
|
23
|
+
Icon: typeof InfoIcon;
|
|
24
|
+
defaultTitle: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VARIANTS: Record<AsideVariant, VariantStyle> = {
|
|
28
|
+
note: {
|
|
29
|
+
bg: "bg-trust-bg",
|
|
30
|
+
shadow: "shadow-clay-blue",
|
|
31
|
+
text: "text-trust",
|
|
32
|
+
Icon: InfoIcon,
|
|
33
|
+
defaultTitle: "Note",
|
|
34
|
+
},
|
|
35
|
+
tip: {
|
|
36
|
+
bg: "bg-surface-light",
|
|
37
|
+
shadow: "shadow-clay-primary",
|
|
38
|
+
text: "text-primary",
|
|
39
|
+
Icon: LightbulbIcon,
|
|
40
|
+
defaultTitle: "Tip",
|
|
41
|
+
},
|
|
42
|
+
caution: {
|
|
43
|
+
bg: "bg-perm-bg",
|
|
44
|
+
shadow: "shadow-clay-amber",
|
|
45
|
+
text: "text-perm",
|
|
46
|
+
Icon: WarningIcon,
|
|
47
|
+
defaultTitle: "Caution",
|
|
48
|
+
},
|
|
49
|
+
danger: {
|
|
50
|
+
bg: "bg-danger-bg",
|
|
51
|
+
shadow: "shadow-clay-danger",
|
|
52
|
+
text: "text-danger",
|
|
53
|
+
Icon: WarningOctagonIcon,
|
|
54
|
+
defaultTitle: "Danger",
|
|
55
|
+
},
|
|
56
|
+
roadmap: {
|
|
57
|
+
bg: "bg-roadmap-bg",
|
|
58
|
+
shadow: "shadow-clay-roadmap",
|
|
59
|
+
text: "text-roadmap",
|
|
60
|
+
Icon: RocketLaunchIcon,
|
|
61
|
+
defaultTitle: "Roadmap",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function Aside({
|
|
66
|
+
type = "note",
|
|
67
|
+
title,
|
|
68
|
+
className,
|
|
69
|
+
children,
|
|
70
|
+
}: {
|
|
71
|
+
type?: AsideVariant;
|
|
72
|
+
title?: string;
|
|
73
|
+
className?: string;
|
|
74
|
+
children: ComponentChildren;
|
|
75
|
+
}) {
|
|
76
|
+
const v = VARIANTS[type];
|
|
77
|
+
const { Icon } = v;
|
|
78
|
+
return (
|
|
79
|
+
<aside
|
|
80
|
+
data-slot="aside"
|
|
81
|
+
className={cn(v.bg, v.shadow, "flex items-start gap-3 rounded-3xl p-4", className)}
|
|
82
|
+
>
|
|
83
|
+
<Icon className={cn(v.text, "mt-0.5 h-6 w-6 shrink-0")} />
|
|
84
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
85
|
+
<p className={cn(v.text, "m-0 text-sm font-extrabold uppercase tracking-wide")}>
|
|
86
|
+
{title ?? v.defaultTitle}
|
|
87
|
+
</p>
|
|
88
|
+
<div className="text-sm leading-relaxed text-foreground [&_code]:rounded [&_code]:bg-background [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_p]:m-0 [&_p+p]:mt-2">
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</aside>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { Aside, type AsideVariant };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import { cn, type ElementProps } from "../utils.ts";
|
|
4
|
+
|
|
5
|
+
// Small status pill (`data-slot="badge"`). Radix's `Slot`/`asChild`
|
|
6
|
+
// polymorphism is dropped — same as this repo's Button/Card, since
|
|
7
|
+
// preact/compat can't take Radix — so a Badge is always a <span>. That also
|
|
8
|
+
// drops shadcn's `ghost`/`link` variants, whose styling is entirely
|
|
9
|
+
// anchor-gated (`[a&]:hover:…`) and inert on a span.
|
|
10
|
+
const badgeVariants = cva(
|
|
11
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
16
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
17
|
+
destructive: "border-transparent bg-destructive text-white",
|
|
18
|
+
outline: "border-border text-foreground",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "default",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function Badge({
|
|
28
|
+
className,
|
|
29
|
+
variant = "default",
|
|
30
|
+
...props
|
|
31
|
+
}: ElementProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
data-slot="badge"
|
|
35
|
+
data-variant={variant}
|
|
36
|
+
className={cn(badgeVariants({ variant, className }))}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { Badge };
|