@moloodev/platform 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,15 @@
1
+ export const builtinEditors = [
2
+ { id: "091d9isv5yp3x5mhwum08p1f", publisherHandle: "moloo", slug: "markdown" },
3
+ { id: "1o5gtual80gchlsd5uug3wfv", publisherHandle: "moloo", slug: "loop" },
4
+ { id: "g6qihgfq096rsp3k1kimttvy", publisherHandle: "moloo", slug: "table" },
5
+ { id: "2cnatugu7hupo4stgh2srv68", publisherHandle: "moloo", slug: "bundle" },
6
+ { id: "uqq597gzhpzkds0mrmc2zdtu", publisherHandle: "moloo", slug: "layout" },
7
+ ] as const;
8
+
9
+ export function builtinEditorId(
10
+ slug: (typeof builtinEditors)[number]["slug"],
11
+ ): string {
12
+ const row = builtinEditors.find((item) => item.slug === slug);
13
+ if (row === undefined) throw new Error(`builtin editor ${slug} is missing`);
14
+ return row.id;
15
+ }
package/src/bundle.ts ADDED
@@ -0,0 +1,217 @@
1
+ import { z } from "zod";
2
+
3
+ export const BUNDLE_SCHEMA_VERSION = 1;
4
+
5
+ export const inclusionSchema = z.enum(["direct", "auto"]);
6
+ export type Inclusion = z.infer<typeof inclusionSchema>;
7
+
8
+ export const entrySchema = z.object({
9
+ inclusion: inclusionSchema,
10
+ revision: z.number().int().min(1).nullable(),
11
+ type: z.string().min(1),
12
+ });
13
+ export type Entry = z.infer<typeof entrySchema>;
14
+
15
+ export type BundleContent = {
16
+ version: typeof BUNDLE_SCHEMA_VERSION;
17
+ entries: Record<string, Entry>;
18
+ };
19
+
20
+ export type BundleContentIssue = {
21
+ severity: "error";
22
+ code:
23
+ | "invalid_document"
24
+ | "unsupported_schema"
25
+ | "invalid_entries"
26
+ | "invalid_asset_id"
27
+ | "invalid_entry"
28
+ | "invalid_inclusion"
29
+ | "invalid_revision"
30
+ | "invalid_type";
31
+ path: readonly (string | number)[];
32
+ };
33
+
34
+ const INCLUSIONS = new Set<Inclusion>(inclusionSchema.options);
35
+
36
+ function record(value: unknown): value is Record<string, unknown> {
37
+ return value !== null && typeof value === "object" && !Array.isArray(value);
38
+ }
39
+
40
+ function identifier(value: unknown): value is string {
41
+ return typeof value === "string" && value.trim().length > 0;
42
+ }
43
+
44
+ function revisionValue(value: unknown): value is number | null {
45
+ if (value === null) return true;
46
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
47
+ }
48
+
49
+ export function blankBundleContent(
50
+ entries: Record<string, Entry> = {},
51
+ ): BundleContent {
52
+ return { version: BUNDLE_SCHEMA_VERSION, entries };
53
+ }
54
+
55
+ export function inspectBundleContent(input: unknown): {
56
+ schemaVersion: number | null;
57
+ issues: readonly BundleContentIssue[];
58
+ references: readonly {
59
+ assetId: string;
60
+ assetType: string;
61
+ path: readonly (string | number)[];
62
+ }[];
63
+ } {
64
+ const issues: BundleContentIssue[] = [];
65
+ const references: {
66
+ assetId: string;
67
+ assetType: string;
68
+ path: readonly (string | number)[];
69
+ }[] = [];
70
+ const issue = (
71
+ code: BundleContentIssue["code"],
72
+ path: readonly (string | number)[],
73
+ ) => {
74
+ issues.push({ severity: "error", code, path });
75
+ };
76
+ if (!record(input)) {
77
+ issue("invalid_document", []);
78
+ return { schemaVersion: null, issues, references };
79
+ }
80
+ const version = input.version;
81
+ const schemaVersion =
82
+ typeof version === "number" && Number.isInteger(version) && version > 0
83
+ ? version
84
+ : null;
85
+ if (version !== BUNDLE_SCHEMA_VERSION) {
86
+ issue(
87
+ schemaVersion === null ? "invalid_document" : "unsupported_schema",
88
+ ["version"],
89
+ );
90
+ return { schemaVersion, issues, references };
91
+ }
92
+ if (!record(input.entries)) {
93
+ issue("invalid_entries", ["entries"]);
94
+ return { schemaVersion: BUNDLE_SCHEMA_VERSION, issues, references };
95
+ }
96
+ for (const [assetId, raw] of Object.entries(input.entries)) {
97
+ const path = ["entries", assetId];
98
+ if (!identifier(assetId)) {
99
+ issue("invalid_asset_id", path);
100
+ continue;
101
+ }
102
+ if (!record(raw)) {
103
+ issue("invalid_entry", path);
104
+ continue;
105
+ }
106
+ if (!INCLUSIONS.has(raw.inclusion as Inclusion)) {
107
+ issue("invalid_inclusion", [...path, "inclusion"]);
108
+ }
109
+ if (!revisionValue(raw.revision)) {
110
+ issue("invalid_revision", [...path, "revision"]);
111
+ }
112
+ if (!identifier(raw.type)) {
113
+ issue("invalid_type", [...path, "type"]);
114
+ }
115
+ if (
116
+ INCLUSIONS.has(raw.inclusion as Inclusion) &&
117
+ revisionValue(raw.revision) &&
118
+ identifier(raw.type)
119
+ ) {
120
+ references.push({
121
+ assetId,
122
+ assetType: raw.type,
123
+ path,
124
+ });
125
+ }
126
+ }
127
+ return { schemaVersion: BUNDLE_SCHEMA_VERSION, issues, references };
128
+ }
129
+
130
+ export function readBundleContent(input: unknown): BundleContent | null {
131
+ return inspectBundleContent(input).issues.length === 0
132
+ ? (input as BundleContent)
133
+ : null;
134
+ }
135
+
136
+ const issueCodeSchema = z.enum([
137
+ "missing",
138
+ "revision_required",
139
+ "revision_missing",
140
+ "trashed",
141
+ "locked",
142
+ "nested",
143
+ ]);
144
+
145
+ export type IssueCode = z.infer<typeof issueCodeSchema>;
146
+
147
+ const issueSchema = z.object({
148
+ assetId: z.string(),
149
+ code: issueCodeSchema,
150
+ });
151
+
152
+ export type Issue = z.infer<typeof issueSchema>;
153
+
154
+ const memberSchema = z.object({
155
+ assetId: z.string(),
156
+ name: z.string(),
157
+ path: z.string(),
158
+ type: z.string(),
159
+ inclusion: inclusionSchema,
160
+ via: z.string().nullable(),
161
+ selected: z.number().nullable(),
162
+ previous: z.number().nullable(),
163
+ latest: z.number(),
164
+ locked: z.boolean(),
165
+ size: z.number(),
166
+ });
167
+
168
+ export type Member = z.infer<typeof memberSchema>;
169
+
170
+ const refSchema = z.object({
171
+ parentId: z.string(),
172
+ childId: z.string(),
173
+ });
174
+
175
+ export type Ref = z.infer<typeof refSchema>;
176
+
177
+ export const compositionSchema = z.object({
178
+ bundleId: z.string(),
179
+ bundleName: z.string(),
180
+ bundleEpoch: z.number(),
181
+ bundleUpdatedAt: z.string(),
182
+ headRevision: z.number(),
183
+ entries: z.record(z.string(), entrySchema),
184
+ members: z.array(memberSchema),
185
+ refs: z.array(refSchema),
186
+ previousMembers: z.array(memberSchema),
187
+ previousRefs: z.array(refSchema),
188
+ added: z.array(z.string()),
189
+ removed: z.array(z.string()),
190
+ issues: z.array(issueSchema),
191
+ reviewed: z.string(),
192
+ totalBytes: z.number(),
193
+ deltaBytes: z.number(),
194
+ });
195
+
196
+ export type Composition = z.infer<typeof compositionSchema>;
197
+
198
+ export const publicationSchema = z.object({
199
+ visibility: z.enum(["private", "public"]),
200
+ });
201
+
202
+ export type Publication = z.infer<typeof publicationSchema>;
203
+
204
+ export const bundleConfirmSchema = z.object({
205
+ composition: compositionSchema,
206
+ revision: z.number().int(),
207
+ });
208
+
209
+ export type BundleConfirm = z.infer<typeof bundleConfirmSchema>;
210
+
211
+ export const DEFAULT_PUBLICATION: Publication = {
212
+ visibility: "private",
213
+ };
214
+
215
+ export function compositionBlocksConfirm(issues: readonly Issue[]): boolean {
216
+ return issues.some((issue) => issue.code !== "locked");
217
+ }
@@ -0,0 +1,14 @@
1
+ /** Reserved basename for empty-directory sentinel assets. */
2
+ export const KEEP_NAME = ".keep";
3
+
4
+ /** Soft-deleted assets older than this are hard-purged. */
5
+ export const TRASH_RETENTION_DAYS = 30;
6
+
7
+ /**
8
+ * Default per-workspace user-content quota (release policy §8): drafts,
9
+ * revisions, and distinct file bytes. Plans and purchases may raise it later.
10
+ */
11
+ export const WORKSPACE_STORAGE_LIMIT_BYTES = 1024 * 1024 * 1024;
12
+
13
+ /** Maximum file size parsed into document content during import. */
14
+ export const IMPORT_MAX_BYTES = 4 * 1024 * 1024;
@@ -0,0 +1,10 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export function readLocalDevCert(): { cert: Buffer; key: Buffer } | undefined {
5
+ const cert = fileURLToPath(new URL("../../../.certs/local.moloo.dev.pem", import.meta.url));
6
+ const key = fileURLToPath(new URL("../../../.certs/local.moloo.dev-key.pem", import.meta.url));
7
+ return existsSync(cert) && existsSync(key)
8
+ ? { cert: readFileSync(cert), key: readFileSync(key) }
9
+ : undefined;
10
+ }
@@ -0,0 +1,113 @@
1
+ export const FILE_SCHEMA_VERSION = 1;
2
+
3
+ /** Per-file cap from the release policy (§7). Enforced before any upload starts. */
4
+ export const MAX_FILE_BYTES = 100 * 1024 * 1024;
5
+
6
+ /**
7
+ * Content of a blob-mode asset: a pointer to immutable bytes. The bytes live in
8
+ * managed object storage; this JSON is what drafts, revisions, holds, and
9
+ * bundles carry. Interpretation metadata (`contentType`, `size`, `sha256`) is
10
+ * fixed with the revision; `fileName` is the uploaded name, kept for exports.
11
+ */
12
+ export type FileContent = {
13
+ version: typeof FILE_SCHEMA_VERSION;
14
+ /** Lowercase hex SHA-256 of the bytes; also the storage key. */
15
+ sha256: string;
16
+ size: number;
17
+ contentType: string;
18
+ fileName: string;
19
+ };
20
+
21
+ export type FileContentIssue = {
22
+ severity: "error";
23
+ code:
24
+ | "invalid_document"
25
+ | "unsupported_schema"
26
+ | "invalid_sha256"
27
+ | "invalid_size"
28
+ | "file_too_large"
29
+ | "invalid_content_type"
30
+ | "invalid_file_name";
31
+ path: readonly (string | number)[];
32
+ };
33
+
34
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
35
+
36
+ export function isSha256Hex(value: unknown): value is string {
37
+ return typeof value === "string" && SHA256_HEX.test(value);
38
+ }
39
+
40
+ export function fileContent(input: Omit<FileContent, "version">): FileContent {
41
+ return { version: FILE_SCHEMA_VERSION, ...input };
42
+ }
43
+
44
+ export function inspectFileContent(input: unknown): {
45
+ schemaVersion: number | null;
46
+ issues: readonly FileContentIssue[];
47
+ } {
48
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
49
+ return {
50
+ schemaVersion: null,
51
+ issues: [{ severity: "error", code: "invalid_document", path: [] }],
52
+ };
53
+ }
54
+ const document = input as Record<string, unknown>;
55
+ if (document.version !== FILE_SCHEMA_VERSION) {
56
+ const version = document.version;
57
+ return {
58
+ schemaVersion:
59
+ typeof version === "number" && Number.isInteger(version) && version > 0
60
+ ? version
61
+ : null,
62
+ issues: [
63
+ { severity: "error", code: "unsupported_schema", path: ["version"] },
64
+ ],
65
+ };
66
+ }
67
+ const issues: FileContentIssue[] = [];
68
+ if (!isSha256Hex(document.sha256))
69
+ issues.push({ severity: "error", code: "invalid_sha256", path: ["sha256"] });
70
+ if (
71
+ typeof document.size !== "number" ||
72
+ !Number.isInteger(document.size) ||
73
+ document.size < 0
74
+ )
75
+ issues.push({ severity: "error", code: "invalid_size", path: ["size"] });
76
+ else if (document.size > MAX_FILE_BYTES)
77
+ issues.push({ severity: "error", code: "file_too_large", path: ["size"] });
78
+ if (typeof document.contentType !== "string" || !document.contentType)
79
+ issues.push({
80
+ severity: "error",
81
+ code: "invalid_content_type",
82
+ path: ["contentType"],
83
+ });
84
+ if (typeof document.fileName !== "string" || !document.fileName.trim())
85
+ issues.push({
86
+ severity: "error",
87
+ code: "invalid_file_name",
88
+ path: ["fileName"],
89
+ });
90
+ return { schemaVersion: FILE_SCHEMA_VERSION, issues };
91
+ }
92
+
93
+ export function readFileContent(
94
+ data: Record<string, unknown> | null | undefined,
95
+ ): FileContent | null {
96
+ if (!data) return null;
97
+ return inspectFileContent(data).issues.length === 0
98
+ ? (data as FileContent)
99
+ : null;
100
+ }
101
+
102
+ export function measureFileContent(
103
+ input: unknown,
104
+ ): { contentType: string; contentBytes: number } | null {
105
+ const content = readFileContent(
106
+ input && typeof input === "object" && !Array.isArray(input)
107
+ ? (input as Record<string, unknown>)
108
+ : null,
109
+ );
110
+ return content
111
+ ? { contentType: content.contentType, contentBytes: content.size }
112
+ : null;
113
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The file origin (`files.moloo.cloud`) contract shared by the platform that
3
+ * issues links and the Worker that serves them. Runtime-agnostic.
4
+ *
5
+ * Storage objects are immutable and named by a random id minted per upload,
6
+ * never by the file name or the digest. The URL is the capability: knowing
7
+ * `/{workspaceId}/{objectId}` is what grants access, exactly as with the
8
+ * platform's Vercel Blob media, so links never expire and cache forever. Bytes
9
+ * disappear only when the platform deletes the object and purges its tags.
10
+ *
11
+ * URL: `/{workspaceId}/{objectId}/{variant}`, plus `?name=<file name>` so a
12
+ * rendered original saves under its name, or `?download=<file name>` to save
13
+ * instead of render. `variant` is `orig` or `w{width}.{format}` from the fixed
14
+ * sets below.
15
+ */
16
+
17
+ /** Resized widths, powers of two so game assets scale cleanly. */
18
+ export const VARIANT_WIDTHS = [256, 512, 1024, 2048] as const;
19
+ export type VariantWidth = (typeof VARIANT_WIDTHS)[number];
20
+
21
+ /** Output of resized variants. Browsers in scope decode AVIF; WebP stays parseable for a fallback. */
22
+ const VARIANT_FORMATS = ["avif", "webp"] as const;
23
+ type VariantFormat = (typeof VARIANT_FORMATS)[number];
24
+ export const DEFAULT_VARIANT_FORMAT: VariantFormat = "avif";
25
+
26
+ /** The one resized variant the platform materializes at upload for instant previews. */
27
+ export const PREVIEW_WIDTH: VariantWidth = 256;
28
+
29
+ /** Cloudflare Images input cap; larger images are served as originals only. */
30
+ const TRANSFORM_MAX_BYTES = 20 * 1024 * 1024;
31
+
32
+ /** Cache purge endpoint on the origin; platform-only, bearer `FILES_ADMIN_KEY`. */
33
+ export const PURGE_PATH = "/_purge";
34
+
35
+ export type Variant =
36
+ | { kind: "orig" }
37
+ | { kind: "resized"; width: VariantWidth; format: VariantFormat };
38
+
39
+ export type DeliveryTarget = {
40
+ workspaceId: string;
41
+ objectId: string;
42
+ variant: Variant;
43
+ };
44
+
45
+ /** `createId()` ids (24 chars) and the digest names of objects stored before ids. */
46
+ const OBJECT_ID = /^[A-Za-z0-9_-]{1,64}$/;
47
+
48
+ export function variantSegment(variant: Variant): string {
49
+ return variant.kind === "orig" ? "orig" : `w${variant.width}.${variant.format}`;
50
+ }
51
+
52
+ export function parseVariant(segment: string): Variant | null {
53
+ if (segment === "orig") return { kind: "orig" };
54
+ const match = /^w(\d+)\.([a-z]+)$/.exec(segment);
55
+ if (!match) return null;
56
+ const width = Number(match[1]);
57
+ const format = match[2];
58
+ if (!(VARIANT_WIDTHS as readonly number[]).includes(width)) return null;
59
+ if (!(VARIANT_FORMATS as readonly string[]).includes(format)) return null;
60
+ return { kind: "resized", width: width as VariantWidth, format: format as VariantFormat };
61
+ }
62
+
63
+ function deliveryPath(target: DeliveryTarget): string {
64
+ return `/${target.workspaceId}/${target.objectId}/${variantSegment(target.variant)}`;
65
+ }
66
+
67
+ export function parseDeliveryPath(pathname: string): DeliveryTarget | null {
68
+ const parts = pathname.split("/");
69
+ if (parts.length !== 4 || parts[0] !== "") return null;
70
+ const [, workspaceId, objectId, segment] = parts as [string, string, string, string];
71
+ if (!OBJECT_ID.test(workspaceId) || !OBJECT_ID.test(objectId)) return null;
72
+ const variant = parseVariant(segment);
73
+ return variant ? { workspaceId, objectId, variant } : null;
74
+ }
75
+
76
+ /**
77
+ * Absolute URL for one variant of an object. `name` labels an inline response
78
+ * so saving it keeps the file name; `download` forces saving under that name.
79
+ */
80
+ export function deliveryUrl(
81
+ origin: string,
82
+ target: DeliveryTarget,
83
+ options: { name?: string; download?: string } = {},
84
+ ): string {
85
+ const url = new URL(deliveryPath(target), origin);
86
+ if (options.download) url.searchParams.set("download", options.download);
87
+ else if (options.name) url.searchParams.set("name", options.name);
88
+ return url.toString();
89
+ }
90
+
91
+ /** The only R2 bucket. Every environment writes here. */
92
+ export const FILES_BUCKET = "moloo-files-prod";
93
+
94
+ /**
95
+ * Cloudflare account that owns `moloo-files-prod`.
96
+ * Same id as `apps/files` wrangler `account_id`.
97
+ */
98
+ export const FILES_ACCOUNT_ID = "a01517696189c6f337df29071a295c33";
99
+
100
+ /** The only file origin. Every environment links here. */
101
+ export const FILES_ORIGIN = "https://files.moloo.cloud";
102
+
103
+ /** Workspace id segment in an object key. No slashes. */
104
+ const WORKSPACE_ID = /^[A-Za-z0-9_-]{1,64}$/;
105
+
106
+ /** Where the original bytes live in the bucket. */
107
+ export function blobKey(workspaceId: string, objectId: string): string {
108
+ return `files/${workspaceId}/${objectId}`;
109
+ }
110
+
111
+ /**
112
+ * Prefix of every object for one workspace, including derivatives
113
+ * (`{objectId}.d/…`). Cleanup may delete only keys under this prefix.
114
+ */
115
+ export function workspaceFilesPrefix(workspaceId: string): string {
116
+ if (!WORKSPACE_ID.test(workspaceId)) {
117
+ throw new Error("workspace id is not a storage key segment");
118
+ }
119
+ return `files/${workspaceId}/`;
120
+ }
121
+
122
+ /** The keys a caller may delete. A key outside the workspace prefix is refused. */
123
+ export function workspaceKeysToDelete(workspaceId: string, keys: readonly string[]): string[] {
124
+ const prefix = workspaceFilesPrefix(workspaceId);
125
+ return keys.map((key) => {
126
+ if (!key.startsWith(prefix)) {
127
+ throw new Error(`refusing to delete outside workspace ${workspaceId}`);
128
+ }
129
+ return key;
130
+ });
131
+ }
132
+
133
+ /** Where a materialized resized variant lives; deleted together with the blob. */
134
+ export function derivativeKey(
135
+ workspaceId: string,
136
+ objectId: string,
137
+ variant: Extract<Variant, { kind: "resized" }>,
138
+ ): string {
139
+ return `${blobKey(workspaceId, objectId)}.d/${variantSegment(variant)}`;
140
+ }
141
+
142
+ /** Every derivative the platform may have written for a blob. */
143
+ export function derivativeKeys(workspaceId: string, objectId: string): string[] {
144
+ return [
145
+ derivativeKey(workspaceId, objectId, {
146
+ kind: "resized",
147
+ width: PREVIEW_WIDTH,
148
+ format: DEFAULT_VARIANT_FORMAT,
149
+ }),
150
+ ];
151
+ }
152
+
153
+ /** Tags on every cached response: purge one object, or a whole workspace. */
154
+ export function cacheTags(workspaceId: string, objectId: string): string[] {
155
+ return [`ws:${workspaceId}`, `obj:${objectId}`];
156
+ }
157
+
158
+ function isImageContentType(contentType: string): boolean {
159
+ return /^image\//i.test(contentType.trim());
160
+ }
161
+
162
+ /**
163
+ * Content-Type bound into the signed PUT. Browsers rewrite `text/*` bodies
164
+ * (charset, newlines) when XHR sees a textual type, so those uploads go as
165
+ * opaque bytes. Media types stay as declared so the file origin can resize
166
+ * images from object metadata. The pointer still records the real MIME.
167
+ */
168
+ export function putContentType(declared: string): string {
169
+ const mime = declared.split(";")[0]!.trim().toLowerCase();
170
+ // XHR rewrites textual types (charset, newline conversion). Store them as bytes.
171
+ if (!mime || mime.startsWith("text/")) return "application/octet-stream";
172
+ return mime;
173
+ }
174
+
175
+ /** Whether resized variants exist for this file: an image small enough to transform. */
176
+ export function canResize(contentType: string, size: number): boolean {
177
+ return isImageContentType(contentType) && size <= TRANSFORM_MAX_BYTES;
178
+ }
179
+
180
+ /** RFC 6266 value: ASCII fallback plus UTF-8 `filename*` for non-ASCII names. */
181
+ export function contentDisposition(
182
+ type: "inline" | "attachment",
183
+ fileName: string,
184
+ ): string {
185
+ const ascii = fileName.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
186
+ return `${type}; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
187
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { customAlphabet } from "nanoid";
2
+
3
+ const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
4
+ const nanoid = customAlphabet(alphabet, 24);
5
+
6
+ /** 24-char lowercase alphanumeric id (nanoid). Shared by every table and CRDT model. */
7
+ export function createId(): string {
8
+ return nanoid();
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ export { type AppIcon } from "./app-icon";
2
+
3
+ export {
4
+ isQualifiedName,
5
+ parseQualifiedName,
6
+ type QualifiedName,
7
+ } from "./qualified-name";
8
+
9
+ export {
10
+ FILE_APPEARANCE_ICONS,
11
+ FIRST_PARTY_SCHEMAS,
12
+ KNOWN_PATTERN_IDS,
13
+ STORAGE_MODES,
14
+ UNKNOWN_FILE_APPEARANCE,
15
+ classifyFile,
16
+ isStorageMode,
17
+ patternForContentType,
18
+ resolveFileAppearance,
19
+ resolvePatternAppearance,
20
+ type AssetTypeIcon,
21
+ type FileAppearance,
22
+ type FileAppearanceMatcher,
23
+ type FirstPartySchemaKey,
24
+ type KnownPatternId,
25
+ type StorageMode,
26
+ } from "./asset-types";
27
+
28
+ export {
29
+ IDENTITY_ISSUER,
30
+ MARKETING_SITE,
31
+ PLATFORM_APPS,
32
+ getPlatformAppById,
33
+ getPlatformAppUrl,
34
+ resolveDevPort,
35
+ resolveSiteOrigin,
36
+ type PlatformApp,
37
+ type PlatformAppId,
38
+ type SiteOriginRef,
39
+ } from "./platform";
40
+
41
+ export {
42
+ getIdAccountUrl,
43
+ getIdOrigin,
44
+ getIdSignInUrl,
45
+ getInviteUrl,
46
+ getMarketingOrigin,
47
+ getWorkspaceUrl,
48
+ } from "./origins";
49
+
50
+ export { resolveSiteEnv, type SiteEnv } from "./url";
51
+
52
+ export {
53
+ PLATFORM_API,
54
+ RESERVED_API_ROOT_SEGMENTS,
55
+ getApiOrigin,
56
+ getDocsOrigin,
57
+ getDocsSurfaceUrl,
58
+ getApiOpenApiUrl,
59
+ getApiOpenApiJsonUrl,
60
+ getPlatformApiBaseUrl,
61
+ resolveApiSurface,
62
+ isProductApiSurface,
63
+ getSurfaceChromeLinks,
64
+ type ApiSurfaceRef,
65
+ } from "./api-surfaces";
66
+
67
+ export {
68
+ WORKSPACE_HOST,
69
+ WORKSPACE_HOST_API,
70
+ getWorkspaceHostUrl,
71
+ normalizeWorkspaceSlug,
72
+ resolveWorkspaceHostDevPort,
73
+ WORKSPACE_SLUG_MAX,
74
+ type WorkspaceSlugInputError,
75
+ } from "./workspace-host";
76
+
77
+ export {
78
+ isTrustedBrowserOrigin,
79
+ matchTrustedOrigin,
80
+ } from "./trusted-origins";
81
+
82
+ export { createId } from "./ids";
83
+ export * from "./constants";
84
+ export * from "./naming";
85
+ export * from "./occupancy-protocol";
86
+ export * from "./file-content";
87
+ export * from "./bundle";
88
+ export * from "./file-delivery";