@maravilla-labs/platform 0.8.1 → 0.9.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # @maravilla-labs/platform
2
+
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 790364b: Add MCP server authoring. Apps can expose AI-agent tools from an `mcp/` folder:
8
+ - `@maravilla-labs/platform/mcp` — new `defineMcpServer` / `defineMcpTool` authoring SDK (`McpToolContext` with the authenticated `user`/`client`, scopes, per-tool `public`, UI templates).
9
+ - `@maravilla-labs/types` — `McpManifest` / `McpToolDescriptor` / `UiTemplateDescriptor` and the `mcp` block on `ManifestV2`.
10
+ - `@maravilla-labs/functions` — `buildMcp` discovery + bundle pipeline emitting `.maravilla/mcp.json`.
11
+ - `@maravilla-labs/adapter-core` — `buildAndIntegrateMcp` and the `mcp` manifest block.
12
+ - `adapter-sveltekit` / `adapter-react-router` — wire MCP integration into the framework build.
13
+
14
+ The runtime serves these tools at `<app-url>/_mcp` as an OAuth-protected MCP endpoint.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [790364b]
19
+ - @maravilla-labs/types@0.9.0
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Proprietary License
2
+
3
+ Copyright (c) 2025 SOLUTAS GmbH, Switzerland. All rights reserved.
4
+
5
+ This software and associated documentation (the "Software") are the confidential and proprietary information of SOLUTAS GmbH ("SOLUTAS"). By using, copying, or accessing the Software, you agree that:
6
+
7
+ - No rights are granted except as expressly set forth in a separate written agreement with SOLUTAS.
8
+ - You may not copy, modify, distribute, sublicense, reverse engineer, or create derivative works of the Software except as permitted in such agreement.
9
+ - The Software is provided "as is" without warranty of any kind; SOLUTAS disclaims all implied warranties to the maximum extent permitted by law.
10
+ - In no event shall SOLUTAS be liable for any damages arising from use of the Software except as expressly provided in an applicable agreement.
11
+
12
+ For licensing inquiries, contact: legal@solutas.ch
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as TransformsConfig } from './transforms-BkgPh93b.js';
2
- export { p as TransformsPatternSpec } from './transforms-BkgPh93b.js';
1
+ import { T as TransformsConfig } from './transforms-DwCPOVTn.js';
2
+ export { a as TransformsPatternSpec } from './transforms-DwCPOVTn.js';
3
3
 
4
4
  /**
5
5
  * @fileoverview Typed schema for `maravilla.config.{ts,yaml,json}` files.
@@ -289,6 +289,42 @@ interface BrandingConfig {
289
289
  /** Raw CSS merged into the hosted auth pages. */
290
290
  custom_css?: string;
291
291
  }
292
+ /**
293
+ * Transactional email settings for the hosted `_auth` flows (currently the
294
+ * password-reset email). Reconciled server-side on deploy.
295
+ *
296
+ * The visible `From` *address* is always the platform's verified sender
297
+ * (Maravilla Cloud) and is not configurable. The `From` display *name* is safe
298
+ * to customise via `from_name`; surface your own address via `reply_to`.
299
+ */
300
+ interface EmailConfig {
301
+ /** Master switch. When `false`, the reset flow still mints a token but no email is sent. Defaults to `true`. */
302
+ enabled?: boolean;
303
+ /** `From` display name (e.g. `"Acme Support"`) → `Acme Support <noreply@maravilla.cloud>`. The address is fixed; only the name is yours. */
304
+ from_name?: string;
305
+ /** Reply-To address. The visible `From` is always the platform sender; replies go here. */
306
+ reply_to?: string;
307
+ /** Override the password-reset email subject line. */
308
+ password_reset_subject?: string;
309
+ /** Full custom HTML body. Use the literal `{{reset_url}}` placeholder for the link. */
310
+ password_reset_html?: string;
311
+ /** Full custom plain-text body (same `{{reset_url}}` placeholder). */
312
+ password_reset_text?: string;
313
+ /** Enable passwordless "magic link" email login on the hosted `/_auth/login` page. Off by default; requires `enabled` to send. */
314
+ magic_link_enabled?: boolean;
315
+ /** Override the magic-link email subject line. */
316
+ magic_link_subject?: string;
317
+ /** Full custom HTML body for the magic-link email. Use the literal `{{magic_link_url}}` placeholder. */
318
+ magic_link_html?: string;
319
+ /** Full custom plain-text body for the magic-link email (same `{{magic_link_url}}` placeholder). */
320
+ magic_link_text?: string;
321
+ /** Override the email-verification email subject line. */
322
+ verification_subject?: string;
323
+ /** Full custom HTML body for the verification email. Use the literal `{{verify_url}}` placeholder. */
324
+ verification_html?: string;
325
+ /** Full custom plain-text body for the verification email (same `{{verify_url}}` placeholder). */
326
+ verification_text?: string;
327
+ }
292
328
  interface AuthConfigBlock {
293
329
  resources?: ResourceDefinition[];
294
330
  groups?: GroupDefinition[];
@@ -297,6 +333,8 @@ interface AuthConfigBlock {
297
333
  oauth?: OAuthProvidersConfig;
298
334
  security?: SecurityConfig;
299
335
  branding?: BrandingConfig;
336
+ /** Transactional email settings for the hosted `_auth` flows. */
337
+ email?: EmailConfig;
300
338
  /**
301
339
  * Named, reusable policy fragments. Reference a fragment from any
302
340
  * resource policy with `fragment('name')`; {@link defineConfig} expands
@@ -327,6 +365,11 @@ interface MaravillaConfig {
327
365
  * matching upload. See {@link TransformsConfig}.
328
366
  */
329
367
  transforms?: TransformsConfig;
368
+ /**
369
+ * MCP server block — turns the app's `mcp/` folder into an MCP server at
370
+ * `<app-url>/_mcp`. See {@link McpConfigBlock}.
371
+ */
372
+ mcp?: McpConfigBlock;
330
373
  }
331
374
  /** MongoDB-style key direction: `1` ascending, `-1` descending. */
332
375
  type IndexDirectionConfig = 1 | -1;
@@ -370,6 +413,42 @@ interface DatabaseConfigBlock {
370
413
  /** sqlite-vec-backed vector indexes. */
371
414
  vectorIndexes?: VectorIndexDeclaration[];
372
415
  }
416
+ /** Server identity advertised to agents in the MCP `initialize` handshake. */
417
+ interface McpServerConfig {
418
+ /** Human-readable server name (e.g. "Notes"). */
419
+ name: string;
420
+ /** Optional semantic version. */
421
+ version?: string;
422
+ /** Optional natural-language usage instructions for the client/model. */
423
+ instructions?: string;
424
+ }
425
+ /** Allow-list of routes that may be framed as MCP UI mini-apps. */
426
+ interface McpUiConfig {
427
+ /** Route globs allowed to render as iframe mini-apps (e.g. `/_mcp/ui/*`). */
428
+ routes: string[];
429
+ }
430
+ /** Whether to persist a user's scope grant so they aren't re-prompted. */
431
+ interface McpConsentConfig {
432
+ remember?: boolean;
433
+ }
434
+ interface McpConfigBlock {
435
+ /** Master switch. With `false` (the default) the `/_mcp` endpoint is off. */
436
+ enabled: boolean;
437
+ /** Server identity advertised to agents. */
438
+ server?: McpServerConfig;
439
+ /**
440
+ * Every scope any tool can require, advertised to agents at login / Dynamic
441
+ * Client Registration. A tool whose `scopes` aren't listed here can never be
442
+ * granted. A tool with no scopes is public (callable without a login).
443
+ */
444
+ scopes?: string[];
445
+ /** Allow-list of UI mini-app routes (frame-ancestors / loopback gate). */
446
+ ui?: McpUiConfig;
447
+ /** Public origin agents connect to, when it differs from the request host. */
448
+ publicUrl?: string;
449
+ /** Consent persistence toggle. */
450
+ consent?: McpConsentConfig;
451
+ }
373
452
  /**
374
453
  * Validate + normalize a Maravilla config.
375
454
  *
@@ -399,4 +478,4 @@ interface DatabaseConfigBlock {
399
478
  */
400
479
  declare function defineConfig(config: MaravillaConfig): MaravillaConfig;
401
480
 
402
- export { type AuthConfigBlock, type BrandingConfig, type DatabaseConfigBlock, type DocumentIndexDeclaration, type GroupDefinition, type GroupPermissionDefinition, type IndexDirectionConfig, type MaravillaConfig, type OAuthProviderDefinition, type OAuthProvidersConfig, type PasswordPolicyDefinition, Policy, type RegistrationConfig, type RegistrationFieldDefinition, type RelationTypeDefinition, type ResourceDefinition, type ResourceServiceType, type SecretRef, type SecurityConfig, type SessionConfigDefinition, TransformsConfig, type VectorIndexDeclaration, type VectorMetricConfig, type VectorStorageConfig, assertNoLegacyShapes, defineConfig, fragment, isAdmin, isStaff, ownsIt, publicWhen, relatesVia };
481
+ export { type AuthConfigBlock, type BrandingConfig, type DatabaseConfigBlock, type DocumentIndexDeclaration, type EmailConfig, type GroupDefinition, type GroupPermissionDefinition, type IndexDirectionConfig, type MaravillaConfig, type McpConfigBlock, type McpConsentConfig, type McpServerConfig, type McpUiConfig, type OAuthProviderDefinition, type OAuthProvidersConfig, type PasswordPolicyDefinition, Policy, type RegistrationConfig, type RegistrationFieldDefinition, type RelationTypeDefinition, type ResourceDefinition, type ResourceServiceType, type SecretRef, type SecurityConfig, type SessionConfigDefinition, TransformsConfig, type VectorIndexDeclaration, type VectorMetricConfig, type VectorStorageConfig, assertNoLegacyShapes, defineConfig, fragment, isAdmin, isStaff, ownsIt, publicWhen, relatesVia };
package/dist/config.js CHANGED
@@ -29,7 +29,6 @@ var Policy = class _Policy {
29
29
  constructor(expr) {
30
30
  this.expr = expr;
31
31
  }
32
- expr;
33
32
  /**
34
33
  * Wrap a raw raisin-rel expression. Lints for legacy footguns and
35
34
  * throws on them (see {@link assertNoLegacyShapes}).
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["/**\n * @fileoverview Typed schema for `maravilla.config.{ts,yaml,json}` files.\n *\n * Declares your project's auth settings (resources, groups, relations,\n * registration fields, OAuth providers, security policy, branding) alongside\n * your code. The Maravilla adapter reads this at build time and reconciles\n * the settings into delivery on deploy.\n *\n * ```typescript\n * import { defineConfig } from '@maravilla-labs/platform/config';\n *\n * export default defineConfig({\n * auth: {\n * resources: [\n * { name: 'todos', title: 'Todos', actions: ['read', 'write'],\n * policy: 'auth.user_id == node.owner' },\n * ],\n * },\n * });\n * ```\n *\n * Omitted sections leave the DB alone — partial adoption is explicitly\n * supported. List-based sections (`resources`, `groups`, `relations`,\n * `oauth`) are upserted and never auto-delete DB-only entries. Singleton\n * sections (`registration`, `security`, `branding`) are replaced wholesale\n * when declared.\n */\n\n/**\n * String value that may either be a literal secret or a reference to an\n * environment variable on the **tenant** (resolved server-side at\n * reconcile time, never shipped plaintext in the manifest).\n *\n * Accepted forms:\n * - `\"literal-value\"` — inline (not recommended for real secrets)\n * - `\"${env.VAR_NAME}\"` — string-template form\n * - `{ env: \"VAR_NAME\" }` — object form\n */\nexport type SecretRef = string | { env: string };\n\nimport type { TransformsConfig } from './transforms.js';\nexport type { TransformsConfig, TransformsPatternSpec } from './transforms.js';\n\n// ════════════════════════════════════════════════════════════════════\n// Typed policy builder (FR-7)\n// ════════════════════════════════════════════════════════════════════\n//\n// A tiny, composable builder for raisin-rel policy expressions. It exists\n// to (a) make the common patterns terse and discoverable and (b) refuse\n// the two footguns that silently fail closed in the runtime:\n// - bare `is_admin` / `auth.isAdmin` / `auth.admin` — use `isAdmin()` /\n// `auth.is_admin`;\n// - `VIA` followed by anything other than a single-quoted bareword —\n// use `relatesVia('NAME')`.\n// The builder emits valid syntax; `Policy.raw(...)` lints hand-written\n// expressions for the same footguns and throws on them.\n\n/**\n * Sentinel wrapping an unexpanded `fragment('name')` reference. Padded\n * with spaces so it composes safely inside `&&`/`||` chains, and chosen\n * so it cannot appear in valid raisin-rel source. It is always replaced\n * by {@link defineConfig} before reaching the runtime.\n */\nconst FRAGMENT_OPEN = ' fragment(';\nconst FRAGMENT_CLOSE = ') ';\n/** Matches the sentinel and captures the fragment NAME. */\nconst FRAGMENT_RE = / fragment\\(([^)]+)\\) /g;\n\n/**\n * Throws when `expr` contains a known legacy/footgun policy shape that the\n * runtime evaluator would fail closed on. Called by {@link Policy.raw}.\n *\n * @internal\n */\nexport function assertNoLegacyShapes(expr: string): void {\n // `auth.isAdmin` / `auth.admin` — camelCase / wrong field. Check before\n // the bare-`is_admin` rule so the message is the most specific one.\n if (/auth\\.isAdmin\\b/.test(expr)) {\n throw new Error(\"Policy: `auth.isAdmin` is invalid — use the `isAdmin()` builder or write `auth.is_admin`.\");\n }\n if (/auth\\.admin\\b/.test(expr)) {\n throw new Error(\"Policy: `auth.admin` is invalid — use the `isAdmin()` builder or write `auth.is_admin`.\");\n }\n // Bare `is_admin` (the valid form is the qualified `auth.is_admin`).\n // Negative lookbehind: any `is_admin` not preceded by a word char or\n // dot is \"bare\". `auth.is_admin` is preceded by `.` and thus allowed.\n if (/(?<![\\w.])is_admin\\b/.test(expr)) {\n throw new Error(\n \"Policy: bare `is_admin` is not a valid root — use the `isAdmin()` builder or write `auth.is_admin`.\",\n );\n }\n // `VIA` must be followed by a single-quoted relation name: `VIA 'NAME'`.\n // Reject `VIA \"NAME\"`, `VIA STEWARDS`, `VIA auth.x`, trailing `VIA`, etc.\n const viaRe = /\\bVIA\\b\\s*([^\\s)]+)?/g;\n let m: RegExpExecArray | null;\n while ((m = viaRe.exec(expr)) !== null) {\n const operand = m[1];\n if (!operand || !/^'[^']+'$/.test(operand)) {\n throw new Error(\n \"Policy: `VIA` must be followed by a single-quoted relation name (e.g. VIA 'STEWARDS'). \" +\n \"Use the `relatesVia('NAME')` builder instead of writing the clause by hand.\",\n );\n }\n }\n}\n\n/**\n * A composable, typed policy expression. Build with the helper functions\n * ({@link ownsIt}, {@link isStaff}, {@link isAdmin}, {@link relatesVia},\n * {@link publicWhen}, {@link fragment}) and combine with `.and()` / `.or()`.\n * `toString()` yields the raisin-rel source.\n */\nexport class Policy {\n private constructor(private readonly expr: string) {}\n\n /**\n * Wrap a raw raisin-rel expression. Lints for legacy footguns and\n * throws on them (see {@link assertNoLegacyShapes}).\n */\n static raw(expr: string): Policy {\n assertNoLegacyShapes(expr);\n return new Policy(expr);\n }\n\n /** @internal Build without linting — for the builder helpers only. */\n private static unchecked(expr: string): Policy {\n return new Policy(expr);\n }\n\n toString(): string {\n return this.expr;\n }\n\n /** Logical OR with another policy (each side parenthesized). */\n or(other: Policy): Policy {\n return Policy.unchecked(`${this.expr} || ${other.expr}`);\n }\n\n /** Logical AND with another policy (each side parenthesized). */\n and(other: Policy): Policy {\n return Policy.unchecked(`${this.expr} && ${other.expr}`);\n }\n}\n\n/**\n * Caller owns the resource: `auth.user_id == node.<field>` (default\n * `owner`).\n */\nexport function ownsIt(field: string = 'owner'): Policy {\n return Policy.raw(`auth.user_id == node.${field}`);\n}\n\n/**\n * Caller is a member of a staff-like group:\n * `auth.roles.contains('<group>')` (default `staff`).\n */\nexport function isStaff(group: string = 'staff'): Policy {\n return Policy.raw(`auth.roles.contains('${group}')`);\n}\n\n/**\n * Caller is a platform admin: `auth.is_admin`. Valid now that the runtime\n * populates `is_admin` from membership in the `admin` group.\n */\nexport function isAdmin(): Policy {\n return Policy.raw('auth.is_admin');\n}\n\n/**\n * A typed `RELATES … VIA '<name>'` clause (FR-1). Emits\n * `<object> RELATES <subject> VIA '<relationName>'[ DEPTH a..b]`.\n *\n * @param relationName - the relation type name (cross-validated against\n * the declared `relations[]` at {@link defineConfig} time).\n * @param opts.subject - the related party (default `auth.user_id`).\n * @param opts.object - the anchor node (default `node.owner`).\n * @param opts.depth - inclusive `[min, max]` traversal depth.\n */\nexport function relatesVia(\n relationName: string,\n opts?: { subject?: string; object?: string; depth?: [number, number] },\n): Policy {\n const subject = opts?.subject ?? 'auth.user_id';\n const object = opts?.object ?? 'node.owner';\n let expr = `${object} RELATES ${subject} VIA '${relationName}'`;\n if (opts?.depth) {\n expr += ` DEPTH ${opts.depth[0]}..${opts.depth[1]}`;\n }\n return Policy.raw(expr);\n}\n\n/**\n * Resource is publicly readable: `node.<field> == true` (default\n * `public`).\n */\nexport function publicWhen(field: string = 'public'): Policy {\n return Policy.raw(`node.${field} == true`);\n}\n\n/**\n * Reference a named fragment declared in `auth.fragments`. Resolved and\n * inlined (parenthesized) by {@link defineConfig}; a dangling reference\n * (no matching fragment) throws there.\n */\nexport function fragment(name: string): Policy {\n // Carries a sentinel that defineConfig() recognizes and expands. It is\n // never emitted to the runtime un-expanded.\n return Policy.raw(`${FRAGMENT_OPEN}${name}${FRAGMENT_CLOSE}`);\n}\n\n// ── Resources + policies ──\n\n/**\n * The platform service this resource binds to. Used by the UI to offer\n * service-correct action presets and policy snippets, and by the reconciler\n * to validate that policies reference legal `node.*` fields for the service.\n *\n * Omit for legacy / cross-service umbrella resources — the runtime falls back\n * to matching purely by `name` (a name collision between e.g. a KV namespace\n * and a DB collection will silently share a policy, which is rarely desired).\n */\nexport type ResourceServiceType =\n | 'kv'\n | 'database'\n | 'realtime'\n | 'media'\n | 'vector'\n | 'storage'\n | 'queue'\n | 'push'\n | 'workflow'\n | 'transforms';\n\nexport interface ResourceDefinition {\n /** URL-safe slug. Used as the resource key in code (e.g. the KV namespace). */\n name: string;\n /** Human-readable title for the admin UI. */\n title: string;\n /** Optional longer description. */\n description?: string;\n /**\n * Which platform service this resource gates. When set, the reconciler\n * validates that the policy only references `node.*` fields legal for that\n * service (e.g. a `realtime` policy can't reference `node.collection`).\n */\n type?: ResourceServiceType;\n /** Actions this resource supports, e.g. `['read', 'write', 'delete']`. */\n actions: string[];\n /**\n * Optional raisin-rel policy expression. Evaluated on every KV/DB/\n * realtime/media op that targets this resource. Leave empty to skip\n * Layer 2 for this resource — tenant + owner isolation still applies.\n *\n * Accepts either a raw string or a typed {@link Policy} built with\n * `ownsIt()`/`isStaff()`/`isAdmin()`/`relatesVia()`/`publicWhen()`/\n * `fragment()` (and `.and()`/`.or()`). {@link defineConfig} serializes\n * it via `.toString()` and expands any `fragment()` references.\n */\n policy?: string | Policy;\n /**\n * C+D read-filter (option ii). JSON object the runtime ANDs into the\n * caller's filter on `db.find` / `db.findOne`. Supports `$auth.<path>`\n * placeholder strings (e.g. `\"$auth.user_id\"`) substituted from the\n * caller's identity at request time. Allowed paths: `user_id`, `email`,\n * `is_admin`, `status`, `email_verified`, `groups`, `roles`, `circles`,\n * `profile.<field>`, `scopes.<field>`.\n *\n * Independent of `policy` — `policy` gates writes and resolved-doc reads;\n * `read_filter` scopes which rows the caller can ever see.\n *\n * Example: `'{\"$or\":[{\"owner\":\"$auth.user_id\"},{\"public\":true}]}'`\n */\n read_filter?: string;\n /**\n * Field-level redaction. Maps a field dot-path to a raisin-rel expression;\n * a field is replaced with null on read when its expression is truthy for\n * the caller (fail-closed). Example: `{ birth_date: \"!auth.is_admin\" }`.\n */\n redact?: Record<string, string>;\n /** When true, every policy decision on this resource is recorded. */\n audit?: boolean;\n /**\n * Fallback decision when this resource has no `policy`: `'allow'` (the\n * default) or `'deny'`. Overrides the tenant-wide `security.default_policy`.\n */\n default_policy?: 'allow' | 'deny';\n}\n\n// ── Groups ──\n\nexport interface GroupPermissionDefinition {\n /** Must match a `ResourceDefinition.name`. */\n resource_name: string;\n /** Actions this group is granted on the resource. */\n actions: string[];\n}\n\nexport interface GroupDefinition {\n /** Unique group name per tenant. */\n name: string;\n /** Tenant-unique slug for stable addressing in JWT/API/policies. */\n slug?: string;\n /** Optional description for the admin UI. */\n description?: string;\n /** Resource permissions granted to the group. Replaces the group's current permissions when declared. */\n permissions?: GroupPermissionDefinition[];\n}\n\n// ── Relations ──\n\nexport interface RelationTypeDefinition {\n /**\n * Uppercase identifier referenced from policies. Don't hand-write the\n * `... VIA 'STEWARDS'` clause — use the typed {@link relatesVia} builder\n * (`relatesVia('STEWARDS')`), which emits the correct single-quoted form\n * and is cross-validated against this list at {@link defineConfig} time.\n */\n relation_name: string;\n /** Tenant-unique slug for stable addressing. */\n slug?: string;\n /** Human-readable title. */\n title: string;\n description?: string;\n /** Grouping for the admin UI (e.g. `\"family\"`, `\"work\"`). */\n category?: string;\n icon?: string;\n color?: string;\n /** Name of the inverse relation type, if one exists. */\n inverse_relation_name?: string;\n /** When true, membership in this relation implies stewardship rights. */\n implies_stewardship?: boolean;\n /** When true, the relation can only target users flagged as minors. */\n requires_minor?: boolean;\n /** When true, the relation is symmetric (A→B implies B→A). */\n bidirectional?: boolean;\n}\n\n// ── Registration fields ──\n\nexport interface RegistrationFieldDefinition {\n /** Field key used as the form field name + in profile data. */\n key: string;\n /** Display label. */\n label: string;\n /** One of: text, email, phone, date, number, select, boolean, url, textarea. */\n field_type: string;\n required: boolean;\n show_on_register: boolean;\n /** Optional validation metadata — passed through to the UI. */\n validation?: Record<string, unknown>;\n}\n\nexport interface RegistrationConfig {\n /** Ordered list of custom registration fields. Declaring this replaces the full list. */\n fields: RegistrationFieldDefinition[];\n}\n\n// ── OAuth providers ──\n\nexport interface OAuthProviderDefinition {\n enabled: boolean;\n client_id: string;\n /** Prefer `{ env: \"VAR_NAME\" }` or `\"${env.VAR_NAME}\"`. */\n client_secret: SecretRef;\n scopes: string[];\n /** Only for `custom_oidc`. */\n discovery_url?: string;\n}\n\nexport interface OAuthProvidersConfig {\n google?: OAuthProviderDefinition;\n github?: OAuthProviderDefinition;\n okta?: OAuthProviderDefinition;\n custom_oidc?: OAuthProviderDefinition;\n}\n\n// ── Security ──\n\nexport interface PasswordPolicyDefinition {\n min_length: number;\n require_uppercase: boolean;\n require_number: boolean;\n require_special: boolean;\n}\n\nexport interface SessionConfigDefinition {\n access_token_ttl_secs: number;\n refresh_token_ttl_secs: number;\n max_sessions_per_user: number;\n require_email_verification: boolean;\n}\n\nexport interface SecurityConfig {\n password_policy?: PasswordPolicyDefinition;\n session?: SessionConfigDefinition;\n}\n\n// ── Branding ──\n\nexport interface BrandingConfig {\n app_name?: string;\n logo_url?: string;\n primary_color?: string;\n secondary_color?: string;\n welcome_message?: string;\n welcome_subtitle?: string;\n /** `\"centered\"`, `\"split-left\"`, `\"split-right\"`, or `\"fullscreen\"`. */\n layout?: string;\n background_image_url?: string;\n /** 0–100 percentage. */\n background_focal_point?: { x: number; y: number };\n background_gradient?: string;\n /** `\"light\"`, `\"dark\"`, or `\"auto\"`. */\n color_mode?: string;\n font_family?: string;\n terms_url?: string;\n privacy_url?: string;\n /** Raw CSS merged into the hosted auth pages. */\n custom_css?: string;\n}\n\n// ── Top-level shape ──\n\nexport interface AuthConfigBlock {\n resources?: ResourceDefinition[];\n groups?: GroupDefinition[];\n relations?: RelationTypeDefinition[];\n registration?: RegistrationConfig;\n oauth?: OAuthProvidersConfig;\n security?: SecurityConfig;\n branding?: BrandingConfig;\n /**\n * Named, reusable policy fragments. Reference a fragment from any\n * resource policy with `fragment('name')`; {@link defineConfig} expands\n * the reference inline (wrapped in parens) at build time, so the\n * runtime never sees the indirection. Values may be a {@link Policy} or\n * a raw string expression.\n *\n * @example\n * ```ts\n * defineConfig({ auth: {\n * fragments: { staffOrAdmin: isStaff().or(isAdmin()) },\n * resources: [{ name: 'todos', title: 'Todos', actions: ['read'],\n * policy: ownsIt().or(fragment('staffOrAdmin')) }],\n * }});\n * ```\n */\n fragments?: Record<string, Policy | string>;\n}\n\nexport interface MaravillaConfig {\n /** All project-level auth settings. Every field is optional — partial adoption is supported. */\n auth?: AuthConfigBlock;\n /** Declarative database indexes (regular + vector). Reconciled upsert-only on deploy. */\n database?: DatabaseConfigBlock;\n /**\n * Declarative media transforms — each entry compiles into a synthetic\n * `storage.put` event handler that fans out the declared\n * `platform.media.transforms.*` calls (via `Promise.all`) for every\n * matching upload. See {@link TransformsConfig}.\n */\n transforms?: TransformsConfig;\n}\n\n// ── Database block ──\n//\n// Regular indexes speed up document reads on frequently-queried fields.\n// Vector indexes back hybrid semantic search via sqlite-vec. Both are\n// upsert-only — declaring an index in config creates it if missing,\n// updates metadata when safe, and never auto-deletes DB-only indexes.\n\n/** MongoDB-style key direction: `1` ascending, `-1` descending. */\nexport type IndexDirectionConfig = 1 | -1;\n\nexport interface DocumentIndexDeclaration {\n /** Collection the index lives on. */\n collection: string;\n /** Optional name; falls back to an auto-derived name. */\n name?: string;\n /**\n * Compound-index key shape. Array of `[field, direction]` tuples\n * preserves ordering, which matters for compound indexes.\n */\n keys: Array<[string, IndexDirectionConfig]> | Record<string, IndexDirectionConfig>;\n unique?: boolean;\n sparse?: boolean;\n /**\n * Partial-index predicate — restricted to inline-literal operators\n * (`$eq`, `$ne`, `$gt`/`$gte`/`$lt`/`$lte`, `$in`/`$nin`, `$exists`,\n * `$and`, `$or`). No `$regex` / `$where` / `$text`.\n */\n partial?: Record<string, unknown>;\n /** TTL in seconds. Requires a single-field index on a unix-seconds field. */\n expireAfterSeconds?: number;\n}\n\n/** Distance metric used by a vector index. */\nexport type VectorMetricConfig = 'cosine' | 'l2' | 'hamming';\n\n/** Storage precision for a vector index. */\nexport type VectorStorageConfig = 'float32' | 'int8' | 'bit';\n\nexport interface VectorIndexDeclaration {\n collection: string;\n field: string;\n dimensions: number;\n metric?: VectorMetricConfig;\n storage?: VectorStorageConfig;\n matryoshka?: boolean;\n multiVector?: boolean;\n}\n\nexport interface DatabaseConfigBlock {\n /** MongoDB-style secondary indexes. */\n indexes?: DocumentIndexDeclaration[];\n /** sqlite-vec-backed vector indexes. */\n vectorIndexes?: VectorIndexDeclaration[];\n}\n\n/**\n * Recursively expand `fragment(name)` sentinels in `expr` against the\n * declared `fragments` map. Each expansion is wrapped in parens so it\n * binds correctly inside `&&`/`||`. Throws on an unknown fragment or a\n * fragment cycle.\n *\n * @internal\n */\nfunction expandFragments(\n expr: string,\n fragments: Record<string, string>,\n seen: ReadonlySet<string> = new Set(),\n): string {\n return expr.replace(FRAGMENT_RE, (_match, rawName: string) => {\n const name = rawName.trim();\n if (!(name in fragments)) {\n throw new Error(\n `Policy: fragment('${name}') is not declared in auth.fragments. ` +\n `Declared fragments: ${Object.keys(fragments).join(', ') || '(none)'}.`,\n );\n }\n if (seen.has(name)) {\n throw new Error(\n `Policy: fragment('${name}') is part of a reference cycle (${[...seen, name].join(' → ')}).`,\n );\n }\n const expanded = expandFragments(fragments[name], fragments, new Set([...seen, name]));\n return `(${expanded.trim()})`;\n });\n}\n\n/** Extract every relation name referenced via `VIA 'NAME'`. @internal */\nfunction relationNamesIn(expr: string): string[] {\n const out: string[] = [];\n const re = /\\bVIA\\s+'([^']+)'/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(expr)) !== null) out.push(m[1]);\n return out;\n}\n\n/** Extract every group referenced via `auth.roles.contains('GROUP')`. @internal */\nfunction groupNamesIn(expr: string): string[] {\n const out: string[] = [];\n const re = /auth\\.roles\\.contains\\(\\s*'([^']+)'\\s*\\)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(expr)) !== null) out.push(m[1]);\n return out;\n}\n\n/**\n * Validate + normalize a Maravilla config.\n *\n * Beyond giving you full IntelliSense, this:\n * - serializes every `Policy`-typed resource policy to its string form;\n * - inlines `fragment('name')` references against `auth.fragments`\n * (throws on an unknown fragment or a cycle);\n * - cross-validates that relation names used in `relatesVia()` exist in\n * the declared `auth.relations[]`, and (when `auth.groups[]` is\n * declared) that groups referenced via `isStaff()` /\n * `auth.roles.contains(...)` exist there — throwing on unknown.\n *\n * The returned config has all policies as plain strings, ready for the\n * reconciler. Sections you didn't declare are left untouched.\n *\n * @example\n * ```typescript\n * import { defineConfig, ownsIt, isStaff } from '@maravilla-labs/platform/config';\n *\n * export default defineConfig({\n * auth: {\n * resources: [{ name: 'todos', title: 'Todos', actions: ['read', 'write'],\n * policy: ownsIt().or(isStaff()) }],\n * },\n * });\n * ```\n */\nexport function defineConfig(config: MaravillaConfig): MaravillaConfig {\n const auth = config.auth;\n if (!auth) return config;\n\n // Normalize the declared fragments to strings up front (a fragment may\n // itself be a Policy or a raw string).\n const fragmentStrings: Record<string, string> = {};\n if (auth.fragments) {\n for (const [name, frag] of Object.entries(auth.fragments)) {\n fragmentStrings[name] = typeof frag === 'string' ? frag : frag.toString();\n }\n }\n\n const declaredRelations = new Set((auth.relations ?? []).map((r) => r.relation_name));\n const declaredGroups = new Set((auth.groups ?? []).map((g) => g.name));\n const validateGroups = (auth.groups ?? []).length > 0;\n\n const resources = auth.resources?.map((res) => {\n if (res.policy == null) return res;\n const raw = typeof res.policy === 'string' ? res.policy : res.policy.toString();\n const expanded = expandFragments(raw, fragmentStrings).trim();\n\n for (const rel of relationNamesIn(expanded)) {\n if (!declaredRelations.has(rel)) {\n throw new Error(\n `Policy for resource '${res.name}' references relation '${rel}' via relatesVia(), ` +\n `but it is not declared in auth.relations[]. ` +\n `Declared relations: ${[...declaredRelations].join(', ') || '(none)'}.`,\n );\n }\n }\n if (validateGroups) {\n for (const group of groupNamesIn(expanded)) {\n if (!declaredGroups.has(group)) {\n throw new Error(\n `Policy for resource '${res.name}' references group '${group}', ` +\n `but it is not declared in auth.groups[]. ` +\n `Declared groups: ${[...declaredGroups].join(', ')}.`,\n );\n }\n }\n }\n\n return { ...res, policy: expanded };\n });\n\n return {\n ...config,\n auth: {\n ...auth,\n ...(resources ? { resources } : {}),\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["/**\n * @fileoverview Typed schema for `maravilla.config.{ts,yaml,json}` files.\n *\n * Declares your project's auth settings (resources, groups, relations,\n * registration fields, OAuth providers, security policy, branding) alongside\n * your code. The Maravilla adapter reads this at build time and reconciles\n * the settings into delivery on deploy.\n *\n * ```typescript\n * import { defineConfig } from '@maravilla-labs/platform/config';\n *\n * export default defineConfig({\n * auth: {\n * resources: [\n * { name: 'todos', title: 'Todos', actions: ['read', 'write'],\n * policy: 'auth.user_id == node.owner' },\n * ],\n * },\n * });\n * ```\n *\n * Omitted sections leave the DB alone — partial adoption is explicitly\n * supported. List-based sections (`resources`, `groups`, `relations`,\n * `oauth`) are upserted and never auto-delete DB-only entries. Singleton\n * sections (`registration`, `security`, `branding`) are replaced wholesale\n * when declared.\n */\n\n/**\n * String value that may either be a literal secret or a reference to an\n * environment variable on the **tenant** (resolved server-side at\n * reconcile time, never shipped plaintext in the manifest).\n *\n * Accepted forms:\n * - `\"literal-value\"` — inline (not recommended for real secrets)\n * - `\"${env.VAR_NAME}\"` — string-template form\n * - `{ env: \"VAR_NAME\" }` — object form\n */\nexport type SecretRef = string | { env: string };\n\nimport type { TransformsConfig } from './transforms.js';\nexport type { TransformsConfig, TransformsPatternSpec } from './transforms.js';\n\n// ════════════════════════════════════════════════════════════════════\n// Typed policy builder (FR-7)\n// ════════════════════════════════════════════════════════════════════\n//\n// A tiny, composable builder for raisin-rel policy expressions. It exists\n// to (a) make the common patterns terse and discoverable and (b) refuse\n// the two footguns that silently fail closed in the runtime:\n// - bare `is_admin` / `auth.isAdmin` / `auth.admin` — use `isAdmin()` /\n// `auth.is_admin`;\n// - `VIA` followed by anything other than a single-quoted bareword —\n// use `relatesVia('NAME')`.\n// The builder emits valid syntax; `Policy.raw(...)` lints hand-written\n// expressions for the same footguns and throws on them.\n\n/**\n * Sentinel wrapping an unexpanded `fragment('name')` reference. Padded\n * with spaces so it composes safely inside `&&`/`||` chains, and chosen\n * so it cannot appear in valid raisin-rel source. It is always replaced\n * by {@link defineConfig} before reaching the runtime.\n */\nconst FRAGMENT_OPEN = ' fragment(';\nconst FRAGMENT_CLOSE = ') ';\n/** Matches the sentinel and captures the fragment NAME. */\nconst FRAGMENT_RE = / fragment\\(([^)]+)\\) /g;\n\n/**\n * Throws when `expr` contains a known legacy/footgun policy shape that the\n * runtime evaluator would fail closed on. Called by {@link Policy.raw}.\n *\n * @internal\n */\nexport function assertNoLegacyShapes(expr: string): void {\n // `auth.isAdmin` / `auth.admin` — camelCase / wrong field. Check before\n // the bare-`is_admin` rule so the message is the most specific one.\n if (/auth\\.isAdmin\\b/.test(expr)) {\n throw new Error(\"Policy: `auth.isAdmin` is invalid — use the `isAdmin()` builder or write `auth.is_admin`.\");\n }\n if (/auth\\.admin\\b/.test(expr)) {\n throw new Error(\"Policy: `auth.admin` is invalid — use the `isAdmin()` builder or write `auth.is_admin`.\");\n }\n // Bare `is_admin` (the valid form is the qualified `auth.is_admin`).\n // Negative lookbehind: any `is_admin` not preceded by a word char or\n // dot is \"bare\". `auth.is_admin` is preceded by `.` and thus allowed.\n if (/(?<![\\w.])is_admin\\b/.test(expr)) {\n throw new Error(\n \"Policy: bare `is_admin` is not a valid root — use the `isAdmin()` builder or write `auth.is_admin`.\",\n );\n }\n // `VIA` must be followed by a single-quoted relation name: `VIA 'NAME'`.\n // Reject `VIA \"NAME\"`, `VIA STEWARDS`, `VIA auth.x`, trailing `VIA`, etc.\n const viaRe = /\\bVIA\\b\\s*([^\\s)]+)?/g;\n let m: RegExpExecArray | null;\n while ((m = viaRe.exec(expr)) !== null) {\n const operand = m[1];\n if (!operand || !/^'[^']+'$/.test(operand)) {\n throw new Error(\n \"Policy: `VIA` must be followed by a single-quoted relation name (e.g. VIA 'STEWARDS'). \" +\n \"Use the `relatesVia('NAME')` builder instead of writing the clause by hand.\",\n );\n }\n }\n}\n\n/**\n * A composable, typed policy expression. Build with the helper functions\n * ({@link ownsIt}, {@link isStaff}, {@link isAdmin}, {@link relatesVia},\n * {@link publicWhen}, {@link fragment}) and combine with `.and()` / `.or()`.\n * `toString()` yields the raisin-rel source.\n */\nexport class Policy {\n private constructor(private readonly expr: string) {}\n\n /**\n * Wrap a raw raisin-rel expression. Lints for legacy footguns and\n * throws on them (see {@link assertNoLegacyShapes}).\n */\n static raw(expr: string): Policy {\n assertNoLegacyShapes(expr);\n return new Policy(expr);\n }\n\n /** @internal Build without linting — for the builder helpers only. */\n private static unchecked(expr: string): Policy {\n return new Policy(expr);\n }\n\n toString(): string {\n return this.expr;\n }\n\n /** Logical OR with another policy (each side parenthesized). */\n or(other: Policy): Policy {\n return Policy.unchecked(`${this.expr} || ${other.expr}`);\n }\n\n /** Logical AND with another policy (each side parenthesized). */\n and(other: Policy): Policy {\n return Policy.unchecked(`${this.expr} && ${other.expr}`);\n }\n}\n\n/**\n * Caller owns the resource: `auth.user_id == node.<field>` (default\n * `owner`).\n */\nexport function ownsIt(field: string = 'owner'): Policy {\n return Policy.raw(`auth.user_id == node.${field}`);\n}\n\n/**\n * Caller is a member of a staff-like group:\n * `auth.roles.contains('<group>')` (default `staff`).\n */\nexport function isStaff(group: string = 'staff'): Policy {\n return Policy.raw(`auth.roles.contains('${group}')`);\n}\n\n/**\n * Caller is a platform admin: `auth.is_admin`. Valid now that the runtime\n * populates `is_admin` from membership in the `admin` group.\n */\nexport function isAdmin(): Policy {\n return Policy.raw('auth.is_admin');\n}\n\n/**\n * A typed `RELATES … VIA '<name>'` clause (FR-1). Emits\n * `<object> RELATES <subject> VIA '<relationName>'[ DEPTH a..b]`.\n *\n * @param relationName - the relation type name (cross-validated against\n * the declared `relations[]` at {@link defineConfig} time).\n * @param opts.subject - the related party (default `auth.user_id`).\n * @param opts.object - the anchor node (default `node.owner`).\n * @param opts.depth - inclusive `[min, max]` traversal depth.\n */\nexport function relatesVia(\n relationName: string,\n opts?: { subject?: string; object?: string; depth?: [number, number] },\n): Policy {\n const subject = opts?.subject ?? 'auth.user_id';\n const object = opts?.object ?? 'node.owner';\n let expr = `${object} RELATES ${subject} VIA '${relationName}'`;\n if (opts?.depth) {\n expr += ` DEPTH ${opts.depth[0]}..${opts.depth[1]}`;\n }\n return Policy.raw(expr);\n}\n\n/**\n * Resource is publicly readable: `node.<field> == true` (default\n * `public`).\n */\nexport function publicWhen(field: string = 'public'): Policy {\n return Policy.raw(`node.${field} == true`);\n}\n\n/**\n * Reference a named fragment declared in `auth.fragments`. Resolved and\n * inlined (parenthesized) by {@link defineConfig}; a dangling reference\n * (no matching fragment) throws there.\n */\nexport function fragment(name: string): Policy {\n // Carries a sentinel that defineConfig() recognizes and expands. It is\n // never emitted to the runtime un-expanded.\n return Policy.raw(`${FRAGMENT_OPEN}${name}${FRAGMENT_CLOSE}`);\n}\n\n// ── Resources + policies ──\n\n/**\n * The platform service this resource binds to. Used by the UI to offer\n * service-correct action presets and policy snippets, and by the reconciler\n * to validate that policies reference legal `node.*` fields for the service.\n *\n * Omit for legacy / cross-service umbrella resources — the runtime falls back\n * to matching purely by `name` (a name collision between e.g. a KV namespace\n * and a DB collection will silently share a policy, which is rarely desired).\n */\nexport type ResourceServiceType =\n | 'kv'\n | 'database'\n | 'realtime'\n | 'media'\n | 'vector'\n | 'storage'\n | 'queue'\n | 'push'\n | 'workflow'\n | 'transforms';\n\nexport interface ResourceDefinition {\n /** URL-safe slug. Used as the resource key in code (e.g. the KV namespace). */\n name: string;\n /** Human-readable title for the admin UI. */\n title: string;\n /** Optional longer description. */\n description?: string;\n /**\n * Which platform service this resource gates. When set, the reconciler\n * validates that the policy only references `node.*` fields legal for that\n * service (e.g. a `realtime` policy can't reference `node.collection`).\n */\n type?: ResourceServiceType;\n /** Actions this resource supports, e.g. `['read', 'write', 'delete']`. */\n actions: string[];\n /**\n * Optional raisin-rel policy expression. Evaluated on every KV/DB/\n * realtime/media op that targets this resource. Leave empty to skip\n * Layer 2 for this resource — tenant + owner isolation still applies.\n *\n * Accepts either a raw string or a typed {@link Policy} built with\n * `ownsIt()`/`isStaff()`/`isAdmin()`/`relatesVia()`/`publicWhen()`/\n * `fragment()` (and `.and()`/`.or()`). {@link defineConfig} serializes\n * it via `.toString()` and expands any `fragment()` references.\n */\n policy?: string | Policy;\n /**\n * C+D read-filter (option ii). JSON object the runtime ANDs into the\n * caller's filter on `db.find` / `db.findOne`. Supports `$auth.<path>`\n * placeholder strings (e.g. `\"$auth.user_id\"`) substituted from the\n * caller's identity at request time. Allowed paths: `user_id`, `email`,\n * `is_admin`, `status`, `email_verified`, `groups`, `roles`, `circles`,\n * `profile.<field>`, `scopes.<field>`.\n *\n * Independent of `policy` — `policy` gates writes and resolved-doc reads;\n * `read_filter` scopes which rows the caller can ever see.\n *\n * Example: `'{\"$or\":[{\"owner\":\"$auth.user_id\"},{\"public\":true}]}'`\n */\n read_filter?: string;\n /**\n * Field-level redaction. Maps a field dot-path to a raisin-rel expression;\n * a field is replaced with null on read when its expression is truthy for\n * the caller (fail-closed). Example: `{ birth_date: \"!auth.is_admin\" }`.\n */\n redact?: Record<string, string>;\n /** When true, every policy decision on this resource is recorded. */\n audit?: boolean;\n /**\n * Fallback decision when this resource has no `policy`: `'allow'` (the\n * default) or `'deny'`. Overrides the tenant-wide `security.default_policy`.\n */\n default_policy?: 'allow' | 'deny';\n}\n\n// ── Groups ──\n\nexport interface GroupPermissionDefinition {\n /** Must match a `ResourceDefinition.name`. */\n resource_name: string;\n /** Actions this group is granted on the resource. */\n actions: string[];\n}\n\nexport interface GroupDefinition {\n /** Unique group name per tenant. */\n name: string;\n /** Tenant-unique slug for stable addressing in JWT/API/policies. */\n slug?: string;\n /** Optional description for the admin UI. */\n description?: string;\n /** Resource permissions granted to the group. Replaces the group's current permissions when declared. */\n permissions?: GroupPermissionDefinition[];\n}\n\n// ── Relations ──\n\nexport interface RelationTypeDefinition {\n /**\n * Uppercase identifier referenced from policies. Don't hand-write the\n * `... VIA 'STEWARDS'` clause — use the typed {@link relatesVia} builder\n * (`relatesVia('STEWARDS')`), which emits the correct single-quoted form\n * and is cross-validated against this list at {@link defineConfig} time.\n */\n relation_name: string;\n /** Tenant-unique slug for stable addressing. */\n slug?: string;\n /** Human-readable title. */\n title: string;\n description?: string;\n /** Grouping for the admin UI (e.g. `\"family\"`, `\"work\"`). */\n category?: string;\n icon?: string;\n color?: string;\n /** Name of the inverse relation type, if one exists. */\n inverse_relation_name?: string;\n /** When true, membership in this relation implies stewardship rights. */\n implies_stewardship?: boolean;\n /** When true, the relation can only target users flagged as minors. */\n requires_minor?: boolean;\n /** When true, the relation is symmetric (A→B implies B→A). */\n bidirectional?: boolean;\n}\n\n// ── Registration fields ──\n\nexport interface RegistrationFieldDefinition {\n /** Field key used as the form field name + in profile data. */\n key: string;\n /** Display label. */\n label: string;\n /** One of: text, email, phone, date, number, select, boolean, url, textarea. */\n field_type: string;\n required: boolean;\n show_on_register: boolean;\n /** Optional validation metadata — passed through to the UI. */\n validation?: Record<string, unknown>;\n}\n\nexport interface RegistrationConfig {\n /** Ordered list of custom registration fields. Declaring this replaces the full list. */\n fields: RegistrationFieldDefinition[];\n}\n\n// ── OAuth providers ──\n\nexport interface OAuthProviderDefinition {\n enabled: boolean;\n client_id: string;\n /** Prefer `{ env: \"VAR_NAME\" }` or `\"${env.VAR_NAME}\"`. */\n client_secret: SecretRef;\n scopes: string[];\n /** Only for `custom_oidc`. */\n discovery_url?: string;\n}\n\nexport interface OAuthProvidersConfig {\n google?: OAuthProviderDefinition;\n github?: OAuthProviderDefinition;\n okta?: OAuthProviderDefinition;\n custom_oidc?: OAuthProviderDefinition;\n}\n\n// ── Security ──\n\nexport interface PasswordPolicyDefinition {\n min_length: number;\n require_uppercase: boolean;\n require_number: boolean;\n require_special: boolean;\n}\n\nexport interface SessionConfigDefinition {\n access_token_ttl_secs: number;\n refresh_token_ttl_secs: number;\n max_sessions_per_user: number;\n require_email_verification: boolean;\n}\n\nexport interface SecurityConfig {\n password_policy?: PasswordPolicyDefinition;\n session?: SessionConfigDefinition;\n}\n\n// ── Branding ──\n\nexport interface BrandingConfig {\n app_name?: string;\n logo_url?: string;\n primary_color?: string;\n secondary_color?: string;\n welcome_message?: string;\n welcome_subtitle?: string;\n /** `\"centered\"`, `\"split-left\"`, `\"split-right\"`, or `\"fullscreen\"`. */\n layout?: string;\n background_image_url?: string;\n /** 0–100 percentage. */\n background_focal_point?: { x: number; y: number };\n background_gradient?: string;\n /** `\"light\"`, `\"dark\"`, or `\"auto\"`. */\n color_mode?: string;\n font_family?: string;\n terms_url?: string;\n privacy_url?: string;\n /** Raw CSS merged into the hosted auth pages. */\n custom_css?: string;\n}\n\n// ── Email ──\n\n/**\n * Transactional email settings for the hosted `_auth` flows (currently the\n * password-reset email). Reconciled server-side on deploy.\n *\n * The visible `From` *address* is always the platform's verified sender\n * (Maravilla Cloud) and is not configurable. The `From` display *name* is safe\n * to customise via `from_name`; surface your own address via `reply_to`.\n */\nexport interface EmailConfig {\n /** Master switch. When `false`, the reset flow still mints a token but no email is sent. Defaults to `true`. */\n enabled?: boolean;\n /** `From` display name (e.g. `\"Acme Support\"`) → `Acme Support <noreply@maravilla.cloud>`. The address is fixed; only the name is yours. */\n from_name?: string;\n /** Reply-To address. The visible `From` is always the platform sender; replies go here. */\n reply_to?: string;\n /** Override the password-reset email subject line. */\n password_reset_subject?: string;\n /** Full custom HTML body. Use the literal `{{reset_url}}` placeholder for the link. */\n password_reset_html?: string;\n /** Full custom plain-text body (same `{{reset_url}}` placeholder). */\n password_reset_text?: string;\n /** Enable passwordless \"magic link\" email login on the hosted `/_auth/login` page. Off by default; requires `enabled` to send. */\n magic_link_enabled?: boolean;\n /** Override the magic-link email subject line. */\n magic_link_subject?: string;\n /** Full custom HTML body for the magic-link email. Use the literal `{{magic_link_url}}` placeholder. */\n magic_link_html?: string;\n /** Full custom plain-text body for the magic-link email (same `{{magic_link_url}}` placeholder). */\n magic_link_text?: string;\n /** Override the email-verification email subject line. */\n verification_subject?: string;\n /** Full custom HTML body for the verification email. Use the literal `{{verify_url}}` placeholder. */\n verification_html?: string;\n /** Full custom plain-text body for the verification email (same `{{verify_url}}` placeholder). */\n verification_text?: string;\n}\n\n// ── Top-level shape ──\n\nexport interface AuthConfigBlock {\n resources?: ResourceDefinition[];\n groups?: GroupDefinition[];\n relations?: RelationTypeDefinition[];\n registration?: RegistrationConfig;\n oauth?: OAuthProvidersConfig;\n security?: SecurityConfig;\n branding?: BrandingConfig;\n /** Transactional email settings for the hosted `_auth` flows. */\n email?: EmailConfig;\n /**\n * Named, reusable policy fragments. Reference a fragment from any\n * resource policy with `fragment('name')`; {@link defineConfig} expands\n * the reference inline (wrapped in parens) at build time, so the\n * runtime never sees the indirection. Values may be a {@link Policy} or\n * a raw string expression.\n *\n * @example\n * ```ts\n * defineConfig({ auth: {\n * fragments: { staffOrAdmin: isStaff().or(isAdmin()) },\n * resources: [{ name: 'todos', title: 'Todos', actions: ['read'],\n * policy: ownsIt().or(fragment('staffOrAdmin')) }],\n * }});\n * ```\n */\n fragments?: Record<string, Policy | string>;\n}\n\nexport interface MaravillaConfig {\n /** All project-level auth settings. Every field is optional — partial adoption is supported. */\n auth?: AuthConfigBlock;\n /** Declarative database indexes (regular + vector). Reconciled upsert-only on deploy. */\n database?: DatabaseConfigBlock;\n /**\n * Declarative media transforms — each entry compiles into a synthetic\n * `storage.put` event handler that fans out the declared\n * `platform.media.transforms.*` calls (via `Promise.all`) for every\n * matching upload. See {@link TransformsConfig}.\n */\n transforms?: TransformsConfig;\n /**\n * MCP server block — turns the app's `mcp/` folder into an MCP server at\n * `<app-url>/_mcp`. See {@link McpConfigBlock}.\n */\n mcp?: McpConfigBlock;\n}\n\n// ── Database block ──\n//\n// Regular indexes speed up document reads on frequently-queried fields.\n// Vector indexes back hybrid semantic search via sqlite-vec. Both are\n// upsert-only — declaring an index in config creates it if missing,\n// updates metadata when safe, and never auto-deletes DB-only indexes.\n\n/** MongoDB-style key direction: `1` ascending, `-1` descending. */\nexport type IndexDirectionConfig = 1 | -1;\n\nexport interface DocumentIndexDeclaration {\n /** Collection the index lives on. */\n collection: string;\n /** Optional name; falls back to an auto-derived name. */\n name?: string;\n /**\n * Compound-index key shape. Array of `[field, direction]` tuples\n * preserves ordering, which matters for compound indexes.\n */\n keys: Array<[string, IndexDirectionConfig]> | Record<string, IndexDirectionConfig>;\n unique?: boolean;\n sparse?: boolean;\n /**\n * Partial-index predicate — restricted to inline-literal operators\n * (`$eq`, `$ne`, `$gt`/`$gte`/`$lt`/`$lte`, `$in`/`$nin`, `$exists`,\n * `$and`, `$or`). No `$regex` / `$where` / `$text`.\n */\n partial?: Record<string, unknown>;\n /** TTL in seconds. Requires a single-field index on a unix-seconds field. */\n expireAfterSeconds?: number;\n}\n\n/** Distance metric used by a vector index. */\nexport type VectorMetricConfig = 'cosine' | 'l2' | 'hamming';\n\n/** Storage precision for a vector index. */\nexport type VectorStorageConfig = 'float32' | 'int8' | 'bit';\n\nexport interface VectorIndexDeclaration {\n collection: string;\n field: string;\n dimensions: number;\n metric?: VectorMetricConfig;\n storage?: VectorStorageConfig;\n matryoshka?: boolean;\n multiVector?: boolean;\n}\n\nexport interface DatabaseConfigBlock {\n /** MongoDB-style secondary indexes. */\n indexes?: DocumentIndexDeclaration[];\n /** sqlite-vec-backed vector indexes. */\n vectorIndexes?: VectorIndexDeclaration[];\n}\n\n// ── MCP block ──\n//\n// The `mcp` block turns the app's `mcp/` folder into an MCP server hosted at\n// `<app-url>/_mcp`. It is the master switch (`enabled`) plus the scope set\n// agents are offered at login — the runtime is the OAuth Authorization Server,\n// so agents authenticate with the app's own accounts (or a headless `mk_…`\n// key). Tools, server identity, and UI templates themselves are authored in\n// `mcp/*.ts` via `defineMcpServer` / `defineMcpTool`. Mirrors the Rust\n// `platform::mcp::McpConfig`.\n\n/** Server identity advertised to agents in the MCP `initialize` handshake. */\nexport interface McpServerConfig {\n /** Human-readable server name (e.g. \"Notes\"). */\n name: string;\n /** Optional semantic version. */\n version?: string;\n /** Optional natural-language usage instructions for the client/model. */\n instructions?: string;\n}\n\n/** Allow-list of routes that may be framed as MCP UI mini-apps. */\nexport interface McpUiConfig {\n /** Route globs allowed to render as iframe mini-apps (e.g. `/_mcp/ui/*`). */\n routes: string[];\n}\n\n/** Whether to persist a user's scope grant so they aren't re-prompted. */\nexport interface McpConsentConfig {\n remember?: boolean;\n}\n\nexport interface McpConfigBlock {\n /** Master switch. With `false` (the default) the `/_mcp` endpoint is off. */\n enabled: boolean;\n /** Server identity advertised to agents. */\n server?: McpServerConfig;\n /**\n * Every scope any tool can require, advertised to agents at login / Dynamic\n * Client Registration. A tool whose `scopes` aren't listed here can never be\n * granted. A tool with no scopes is public (callable without a login).\n */\n scopes?: string[];\n /** Allow-list of UI mini-app routes (frame-ancestors / loopback gate). */\n ui?: McpUiConfig;\n /** Public origin agents connect to, when it differs from the request host. */\n publicUrl?: string;\n /** Consent persistence toggle. */\n consent?: McpConsentConfig;\n}\n\n/**\n * Recursively expand `fragment(name)` sentinels in `expr` against the\n * declared `fragments` map. Each expansion is wrapped in parens so it\n * binds correctly inside `&&`/`||`. Throws on an unknown fragment or a\n * fragment cycle.\n *\n * @internal\n */\nfunction expandFragments(\n expr: string,\n fragments: Record<string, string>,\n seen: ReadonlySet<string> = new Set(),\n): string {\n return expr.replace(FRAGMENT_RE, (_match, rawName: string) => {\n const name = rawName.trim();\n if (!(name in fragments)) {\n throw new Error(\n `Policy: fragment('${name}') is not declared in auth.fragments. ` +\n `Declared fragments: ${Object.keys(fragments).join(', ') || '(none)'}.`,\n );\n }\n if (seen.has(name)) {\n throw new Error(\n `Policy: fragment('${name}') is part of a reference cycle (${[...seen, name].join(' → ')}).`,\n );\n }\n const expanded = expandFragments(fragments[name], fragments, new Set([...seen, name]));\n return `(${expanded.trim()})`;\n });\n}\n\n/** Extract every relation name referenced via `VIA 'NAME'`. @internal */\nfunction relationNamesIn(expr: string): string[] {\n const out: string[] = [];\n const re = /\\bVIA\\s+'([^']+)'/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(expr)) !== null) out.push(m[1]);\n return out;\n}\n\n/** Extract every group referenced via `auth.roles.contains('GROUP')`. @internal */\nfunction groupNamesIn(expr: string): string[] {\n const out: string[] = [];\n const re = /auth\\.roles\\.contains\\(\\s*'([^']+)'\\s*\\)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(expr)) !== null) out.push(m[1]);\n return out;\n}\n\n/**\n * Validate + normalize a Maravilla config.\n *\n * Beyond giving you full IntelliSense, this:\n * - serializes every `Policy`-typed resource policy to its string form;\n * - inlines `fragment('name')` references against `auth.fragments`\n * (throws on an unknown fragment or a cycle);\n * - cross-validates that relation names used in `relatesVia()` exist in\n * the declared `auth.relations[]`, and (when `auth.groups[]` is\n * declared) that groups referenced via `isStaff()` /\n * `auth.roles.contains(...)` exist there — throwing on unknown.\n *\n * The returned config has all policies as plain strings, ready for the\n * reconciler. Sections you didn't declare are left untouched.\n *\n * @example\n * ```typescript\n * import { defineConfig, ownsIt, isStaff } from '@maravilla-labs/platform/config';\n *\n * export default defineConfig({\n * auth: {\n * resources: [{ name: 'todos', title: 'Todos', actions: ['read', 'write'],\n * policy: ownsIt().or(isStaff()) }],\n * },\n * });\n * ```\n */\nexport function defineConfig(config: MaravillaConfig): MaravillaConfig {\n const auth = config.auth;\n if (!auth) return config;\n\n // Normalize the declared fragments to strings up front (a fragment may\n // itself be a Policy or a raw string).\n const fragmentStrings: Record<string, string> = {};\n if (auth.fragments) {\n for (const [name, frag] of Object.entries(auth.fragments)) {\n fragmentStrings[name] = typeof frag === 'string' ? frag : frag.toString();\n }\n }\n\n const declaredRelations = new Set((auth.relations ?? []).map((r) => r.relation_name));\n const declaredGroups = new Set((auth.groups ?? []).map((g) => g.name));\n const validateGroups = (auth.groups ?? []).length > 0;\n\n const resources = auth.resources?.map((res) => {\n if (res.policy == null) return res;\n const raw = typeof res.policy === 'string' ? res.policy : res.policy.toString();\n const expanded = expandFragments(raw, fragmentStrings).trim();\n\n for (const rel of relationNamesIn(expanded)) {\n if (!declaredRelations.has(rel)) {\n throw new Error(\n `Policy for resource '${res.name}' references relation '${rel}' via relatesVia(), ` +\n `but it is not declared in auth.relations[]. ` +\n `Declared relations: ${[...declaredRelations].join(', ') || '(none)'}.`,\n );\n }\n }\n if (validateGroups) {\n for (const group of groupNamesIn(expanded)) {\n if (!declaredGroups.has(group)) {\n throw new Error(\n `Policy for resource '${res.name}' references group '${group}', ` +\n `but it is not declared in auth.groups[]. ` +\n `Declared groups: ${[...declaredGroups].join(', ')}.`,\n );\n }\n }\n }\n\n return { ...res, policy: expanded };\n });\n\n return {\n ...config,\n auth: {\n ...auth,\n ...(resources ? { resources } : {}),\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/events.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { b as RenEvent } from './ren-DrYefHO5.js';
1
+ import { R as RenEvent } from './ren-DetHRhCL.js';
2
2
 
3
3
  /**
4
4
  * @fileoverview Event handler registration helpers for Maravilla.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { DbDocument, AuthService, AuthUser } from '@maravilla-labs/types';
2
- export { ActAsContext, AddCircleMemberOptions, AddRelationOptions, AuthCaller, AuthCircle, AuthConfig, AuthField, AuthGroup, AuthService, AuthSession, AuthStewardshipApi, AuthUser, CanCheck, CircleMembership, CreateCircleOptions, CreateGroupOptions, CreateManagedUserOptions, CreateRelationTypeOptions, CreateResourceOptions, CreateStewardshipOverrideOptions, DbDocument, DelegationMode, GroupPermission, ListRelationsOptions, LoginOptions, PolicyExplain, RegisterOptions, Relation, RelationListDirection, RelationType, Resource, ResourceServiceType, ScopedPermission, StewardshipAuditEntry, StewardshipOverride, StewardshipResolution, StewardshipStatus, UpdateCircleOptions, UpdateGroupOptions, UpdateRelationTypeOptions, UpdateResourceOptions, UpdateUserOptions, UserListFilter, UserListResponse } from '@maravilla-labs/types';
3
- export { R as RenClient, a as RenClientOptions, b as RenEvent, g as getOrCreateClientId, r as renFetch, s as storageDelete, c as storageUpload } from './ren-DrYefHO5.js';
1
+ import { DbDocument, BrowserClient, FramesClient, AuthService, AuthUser } from '@maravilla-labs/types';
2
+ export { ActAsContext, AddCircleMemberOptions, AddRelationOptions, AuthCaller, AuthCircle, AuthConfig, AuthField, AuthGroup, AuthService, AuthSession, AuthStewardshipApi, AuthUser, BrowserClient, BrowserCookie, BrowserJobHandle, BrowserJobStatus, BrowserViewport, BrowserWaitFor, CanCheck, CircleMembership, CreateCircleOptions, CreateGroupOptions, CreateManagedUserOptions, CreateRelationTypeOptions, CreateResourceOptions, CreateStewardshipOverrideOptions, DbDocument, DelegationMode, FramesClient, FramesCodec, FramesRenderRequest, GroupPermission, ListRelationsOptions, LoginOptions, PdfPageFormat, PdfRequest, PolicyExplain, RegisterOptions, Relation, RelationListDirection, RelationType, Resource, ResourceServiceType, ScopedPermission, ScreenshotFormat, ScreenshotRequest, StewardshipAuditEntry, StewardshipOverride, StewardshipResolution, StewardshipStatus, UpdateCircleOptions, UpdateGroupOptions, UpdateRelationTypeOptions, UpdateResourceOptions, UpdateUserOptions, UserListFilter, UserListResponse } from '@maravilla-labs/types';
3
+ export { b as RenClient, a as RenClientOptions, R as RenEvent, g as getOrCreateClientId, r as renFetch, c as storageDelete, s as storageUpload } from './ren-DetHRhCL.js';
4
4
  import { LocalParticipant } from 'livekit-client';
5
5
  export { RegisterPushOptions, RegisterPushResult, offsetBefore, registerPush, unregisterPush } from './push.js';
6
- export { D as DocConvertOpts, a as DocFormat, b as DocInsertQrCodeOpts, c as DocReplaceImagesOpts, d as DocTemplateMergeOpts, e as DocThumbnailOpts, f as DocToHtmlOpts, g as DocToMarkdownOpts, h as DocToPdfOpts, I as ImageFormat, i as ImageRef, J as JobHandle, j as JobStatus, k as JobStatusResponse, M as MediaInfo, O as OcrOpts, Q as QrCodeSpec, l as QrPayload, R as ResizeOpts, T as ThumbnailOpts, m as TranscodeOpts, n as TransformSpec, o as TransformsConfig, p as TransformsPatternSpec, q as TransformsService, V as VideoFormat, r as keyFor, t as transforms } from './transforms-BkgPh93b.js';
6
+ export { f as DocConvertOpts, D as DocFormat, k as DocInsertQrCodeOpts, j as DocReplaceImagesOpts, m as DocTemplateMergeOpts, e as DocThumbnailOpts, h as DocToHtmlOpts, g as DocToMarkdownOpts, d as DocToPdfOpts, I as ImageFormat, i as ImageRef, o as JobHandle, J as JobStatus, p as JobStatusResponse, M as MediaInfo, O as OcrOpts, Q as QrCodeSpec, l as QrPayload, R as ResizeOpts, c as ThumbnailOpts, b as TranscodeOpts, n as TransformSpec, T as TransformsConfig, a as TransformsPatternSpec, q as TransformsService, V as VideoFormat, r as keyFor, t as transforms } from './transforms-DwCPOVTn.js';
7
7
 
8
8
  /**
9
9
  * Media service for video/audio room management.
@@ -776,6 +776,19 @@ interface PlatformEnv {
776
776
  DB: Database;
777
777
  /** Object/file storage interface */
778
778
  STORAGE: Storage;
779
+ /**
780
+ * Headless-browser screenshot / PDF render. Returns a job handle
781
+ * polled via the shared transforms job ledger. Phase 1 plumbing stub —
782
+ * the worker marks browser jobs `failed("not implemented")` until
783
+ * Task #2 lands the Chromium pipeline.
784
+ */
785
+ BROWSER: BrowserClient;
786
+ /**
787
+ * Deterministic HTML → MP4 / WebM render. Path-only: the renderer hits
788
+ * a route on the tenant's own deployment. Phase 1 plumbing stub —
789
+ * real implementation in Task #3.
790
+ */
791
+ FRAMES: FramesClient;
779
792
  }
780
793
  /**
781
794
  * Main platform interface providing access to all Maravilla runtime services.
package/dist/index.js CHANGED
@@ -76,8 +76,6 @@ var RemoteMediaService = class {
76
76
  this.baseUrl = baseUrl;
77
77
  this.headers = headers;
78
78
  }
79
- baseUrl;
80
- headers;
81
79
  async fetch(url, options = {}) {
82
80
  const { getRequestAuthHeader: getRequestAuthHeader2 } = await Promise.resolve().then(() => (init_request_scope(), request_scope_exports));
83
81
  const response = await fetch(url, {
@@ -131,9 +129,6 @@ var RemoteKvNamespace = class {
131
129
  this.namespace = namespace;
132
130
  this.headers = headers;
133
131
  }
134
- baseUrl;
135
- namespace;
136
- headers;
137
132
  /**
138
133
  * Internal method for making HTTP requests to the dev server.
139
134
  * Handles error responses and authentication headers.
@@ -190,8 +185,6 @@ var RemoteDatabase = class {
190
185
  this.baseUrl = baseUrl;
191
186
  this.headers = headers;
192
187
  }
193
- baseUrl;
194
- headers;
195
188
  /**
196
189
  * Internal method for making HTTP requests to the dev server.
197
190
  * Handles error responses and authentication headers.
@@ -301,8 +294,6 @@ var RemoteStorage = class _RemoteStorage {
301
294
  this.baseUrl = baseUrl;
302
295
  this.headers = headers;
303
296
  }
304
- baseUrl;
305
- headers;
306
297
  /**
307
298
  * Internal method for making HTTP requests to the dev server.
308
299
  * Handles error responses and authentication headers.
@@ -469,8 +460,6 @@ var RemotePresenceService = class {
469
460
  this.baseUrl = baseUrl;
470
461
  this.headers = headers;
471
462
  }
472
- baseUrl;
473
- headers;
474
463
  async join(channel, userId, metadata) {
475
464
  const res = await fetch(`${this.baseUrl}/api/realtime/presence/${encodeURIComponent(channel)}/join`, {
476
465
  method: "POST",
@@ -503,8 +492,6 @@ var RemoteRealtimeService = class {
503
492
  this.headers = headers;
504
493
  this.presence = new RemotePresenceService(baseUrl, headers);
505
494
  }
506
- baseUrl;
507
- headers;
508
495
  presence;
509
496
  async publish(channel, data, options) {
510
497
  await fetch(`${this.baseUrl}/api/realtime/publish`, {
@@ -526,8 +513,6 @@ var RemoteAuthService = class _RemoteAuthService {
526
513
  this.baseUrl = baseUrl;
527
514
  this.headers = headers;
528
515
  }
529
- baseUrl;
530
- headers;
531
516
  async post(path, body) {
532
517
  const res = await fetch(`${this.baseUrl}/api/platform/auth${path}`, {
533
518
  method: "POST",
@@ -894,8 +879,6 @@ var RemoteWorkflows = class {
894
879
  this.baseUrl = baseUrl;
895
880
  this.headers = headers;
896
881
  }
897
- baseUrl;
898
- headers;
899
882
  async request(path, options = {}) {
900
883
  const response = await fetch(`${this.baseUrl}${path}`, {
901
884
  ...options,
@@ -1009,11 +992,15 @@ function createRemoteClient(baseUrl, tenant) {
1009
992
  const auth = new RemoteAuthService(baseUrl, headers);
1010
993
  const policy = new RemotePolicyService();
1011
994
  const workflows = new RemoteWorkflows(baseUrl, headers);
995
+ const browser = new RemoteBrowserClient(baseUrl, headers);
996
+ const frames = new RemoteFramesClient(baseUrl, headers);
1012
997
  return {
1013
998
  env: {
1014
999
  KV: kvProxy,
1015
1000
  DB: db,
1016
- STORAGE: storage
1001
+ STORAGE: storage,
1002
+ BROWSER: browser,
1003
+ FRAMES: frames
1017
1004
  },
1018
1005
  media,
1019
1006
  realtime,
@@ -1022,13 +1009,53 @@ function createRemoteClient(baseUrl, tenant) {
1022
1009
  workflows
1023
1010
  };
1024
1011
  }
1012
+ var RemoteBrowserClient = class {
1013
+ constructor(baseUrl, headers) {
1014
+ this.baseUrl = baseUrl;
1015
+ this.headers = headers;
1016
+ }
1017
+ async post(path, body) {
1018
+ const res = await fetch(`${this.baseUrl}/api/media/browser${path}`, {
1019
+ method: "POST",
1020
+ headers: { "Content-Type": "application/json", ...this.headers, ...getRequestAuthHeader() },
1021
+ body: JSON.stringify(body)
1022
+ });
1023
+ if (!res.ok) {
1024
+ const text = await res.text();
1025
+ throw new Error(`Browser error (${res.status}): ${text}`);
1026
+ }
1027
+ return res.json();
1028
+ }
1029
+ screenshot(request) {
1030
+ return this.post("/screenshot", request);
1031
+ }
1032
+ pdf(request) {
1033
+ return this.post("/pdf", request);
1034
+ }
1035
+ };
1036
+ var RemoteFramesClient = class {
1037
+ constructor(baseUrl, headers) {
1038
+ this.baseUrl = baseUrl;
1039
+ this.headers = headers;
1040
+ }
1041
+ async render(request) {
1042
+ const res = await fetch(`${this.baseUrl}/api/media/frames/render`, {
1043
+ method: "POST",
1044
+ headers: { "Content-Type": "application/json", ...this.headers, ...getRequestAuthHeader() },
1045
+ body: JSON.stringify(request)
1046
+ });
1047
+ if (!res.ok) {
1048
+ const text = await res.text();
1049
+ throw new Error(`Frames render error (${res.status}): ${text}`);
1050
+ }
1051
+ return res.json();
1052
+ }
1053
+ };
1025
1054
  var RemoteTransformsService = class {
1026
1055
  constructor(baseUrl, headers) {
1027
1056
  this.baseUrl = baseUrl;
1028
1057
  this.headers = headers;
1029
1058
  }
1030
- baseUrl;
1031
- headers;
1032
1059
  async post(path, body) {
1033
1060
  const res = await fetch(`${this.baseUrl}/api/media/transforms${path}`, {
1034
1061
  method: "POST",