@claxedo/sandbox-manager 0.6.0 → 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.
Files changed (41) hide show
  1. package/README.md +60 -15
  2. package/dist/checkpoint-manager.d.ts +47 -0
  3. package/dist/checkpoint-manager.mjs +116 -0
  4. package/dist/daytona-allow-list.d.ts +11 -0
  5. package/dist/daytona-allow-list.mjs +5 -1
  6. package/dist/driver-catalog.d.ts +5 -52
  7. package/dist/driver-catalog.mjs +263 -121
  8. package/dist/drivers/box.mjs +309 -39
  9. package/dist/drivers/cloudflare.d.ts +18 -7
  10. package/dist/drivers/cloudflare.mjs +423 -34
  11. package/dist/drivers/daytona.d.ts +82 -13
  12. package/dist/drivers/daytona.mjs +495 -45
  13. package/dist/drivers/docker.mjs +284 -38
  14. package/dist/drivers/exe.d.ts +26 -0
  15. package/dist/drivers/exe.mjs +492 -0
  16. package/dist/drivers/fetch-bridge.mjs +29 -5
  17. package/dist/drivers/modal.d.ts +2 -5
  18. package/dist/drivers/modal.mjs +296 -46
  19. package/dist/drivers/transient-error.d.ts +11 -0
  20. package/dist/drivers/vercel.d.ts +27 -11
  21. package/dist/drivers/vercel.mjs +339 -59
  22. package/dist/hosted-network-policy.d.ts +87 -0
  23. package/dist/hosted-network-policy.mjs +61 -0
  24. package/dist/image-name.mjs +34 -0
  25. package/dist/image.d.ts +18 -3
  26. package/dist/image.mjs +47 -32
  27. package/dist/index.d.ts +281 -17
  28. package/dist/index.mjs +393 -26
  29. package/dist/json.d.ts +13 -0
  30. package/dist/lease-policy.d.ts +6 -3
  31. package/dist/lease-policy.mjs +213 -54
  32. package/dist/lease-types.d.ts +17 -3
  33. package/dist/runtime-env.d.ts +12 -1
  34. package/dist/runtime-env.mjs +14 -0
  35. package/dist/runtime-version.mjs +1 -9
  36. package/dist/service-url-exposure.d.ts +1 -2
  37. package/dist/service-url-exposure.mjs +1 -17
  38. package/dist/stores/memory.mjs +39 -4
  39. package/package.json +67 -51
  40. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  41. package/dist/drivers/cloudflare-egress.mjs +0 -113
package/README.md CHANGED
@@ -11,7 +11,7 @@ The package owns the generic pieces:
11
11
  - `SandboxDriver`
12
12
  - provider drivers for Cloudflare, Daytona, Docker, fetch bridge, Modal, and Vercel
13
13
 
14
- It deliberately does not own Claxedo product auth, billing, Convex schema, SQLite app storage, routes, or relay tokens. Applications provide those through adapters and call `createSandboxManager`.
14
+ It deliberately does not own Claxedo product auth, billing, app storage schema, routes, or relay tokens. Applications provide those through adapters and call `createSandboxManager`.
15
15
 
16
16
  ## Install
17
17
 
@@ -59,8 +59,8 @@ Swap `createDockerSandboxDriver` for `createDaytonaSandboxDriver`,
59
59
  `@claxedo/sandbox-manager/drivers/*`) to place on a hosted provider instead —
60
60
  see [`docs/architecture.md`](docs/architecture.md) for the full driver
61
61
  comparison and each provider's required options. Swap `createMemoryLeaseStore`
62
- for a persisted `SandboxLeaseStore` implementation (e.g. backed by SQLite or
63
- Convex) to survive process restarts.
62
+ for a persisted `SandboxLeaseStore` implementation (e.g. backed by SQLite) to
63
+ survive process restarts.
64
64
 
65
65
  ## Credentials & secrets
66
66
 
@@ -99,26 +99,71 @@ Per-provider mechanism (from each provider's official docs):
99
99
  | Driver | `secretBrokering` | Mechanism |
100
100
  | --- | --- | --- |
101
101
  | Daytona | `native` | `daytona.secret.create({name, value, hosts})`; the sandbox env holds an opaque `dtn_secret_…` placeholder and an egress proxy substitutes the real value only for allowlisted hosts ([docs](https://www.daytona.io/docs/en/secrets/)). |
102
- | Vercel | `native` | Firewall header-transform (`updateNetworkPolicy`): the value is spliced onto egress to the allowlisted hosts as `header`; the sandbox makes an unauthenticated request, egress restricted to brokered hosts ([docs](https://vercel.com/docs/sandbox/concepts/firewall)). |
103
- | Cloudflare | `proxy` | Implemented via the official [Worker-proxy pattern](https://developers.cloudflare.com/sandbox/guides/proxy-requests/): the driver sends the value to the sandbox Worker (API-token-gated, never into the container); the Worker stores it in KV, mints a short-lived per-sandbox JWT, and sets `CLAXEDO_EGRESS_PROXY_URL`/`CLAXEDO_EGRESS_TOKEN`/`CLAXEDO_EGRESS_HOSTS` in the container. The sandbox routes brokered-host egress through the Worker, which validates the JWT and injects the credential as `header` — the raw value never enters the sandbox. The reusable core is `@claxedo/sandbox-manager/drivers/cloudflare-egress` (mint/verify + `handleEgressRequest`). |
102
+ | Vercel | `native` | Firewall header-transform (`updateNetworkPolicy`): the value is spliced onto egress to the allowlisted hosts as `header`, so the sandbox makes an unauthenticated request ([docs](https://vercel.com/docs/sandbox/concepts/firewall)). `updateNetworkPolicy` replaces the whole policy, so the driver sends the **union** of the create-time allow-list and the brokered hosts — attaching a credential must not revoke egress the caller was already granted (a mid-run `npm install` would start failing), nor widen it to a brokered host the create-time policy never approved. |
103
+ | Cloudflare | `native` | The driver sends named registrations to the API-token-gated Worker. KV holds the values outside the container; native HTTPS outbound handlers replace named placeholders in registered headers. Each request reads KV, so there is no expiring container JWT. KV propagation delay still applies to rotation and withdrawal. |
104
104
  | Modal | `none` | Modal [Secrets](https://modal.com/docs/guide/secrets) are an encrypted by-reference store, but exposed as **readable env vars** inside the sandbox — they cannot satisfy the never-readable contract. |
105
105
  | Docker, fetch | `none` | Plaintext env only. |
106
106
 
107
107
  **Fail-closed contract.** A brokered secret is never silently downgraded to
108
- readable env. `native` (transparent egress) and `proxy` (Cloudflare Worker
109
- egress proxy) both keep the value out of the sandbox and provision normally;
110
- only a `none` driver makes the manager refuse to provision (`status:
108
+ readable env. Only `native` keeps the value out of the sandbox and provisions
109
+ normally; a `none` driver makes the manager refuse to provision (`status:
111
110
  "unavailable", error: "secret_brokering_unsupported"`) rather than expose or
112
111
  drop the credential. Brokered secret values are also never written to labels,
113
112
  never logged, and never captured in a driver snapshot.
114
113
 
115
- > Consumption note: `native` brokering is transparent (existing code reaches
116
- > the real host and the platform injects). Cloudflare `proxy` is NOT
117
- > transparent — the sandbox must route brokered-host requests through
118
- > `CLAXEDO_EGRESS_PROXY_URL` (with the `CLAXEDO_EGRESS_TOKEN` bearer and the
119
- > `x-claxedo-egress-target` header). Modal stays `none`; its secret store
120
- > improves storage hygiene for *readable* credentials but does not hide them
121
- > from the sandbox.
114
+ > Native brokering keeps the original upstream URL. Cloudflare accepts its
115
+ > named placeholder as the complete credential header, or with a Bearer prefix
116
+ > for Authorization API-key clients. Modal remains `none`: its encrypted store
117
+ > exposes credentials as readable environment variables inside the sandbox.
118
+
119
+ ## Egress containment
120
+
121
+ `SandboxManagerInput.net` restricts what the sandbox can reach on the network.
122
+ It is a *separate* control from brokered secrets, with a **different** posture,
123
+ and the difference matters:
124
+
125
+ - **Brokered secrets fail closed.** A driver that cannot broker refuses to
126
+ provision (`error: "secret_brokering_unsupported"`).
127
+ - **Egress does not.** Only `daytona` (names + CIDRs) and `vercel` (names) can
128
+ enforce an allowlist. Every other driver declares
129
+ `metadata.egressControl: "none"`, and for those the manager **withholds** the
130
+ policy and provisions anyway — the sandbox runs with unrestricted egress.
131
+
132
+ Withholding rather than passing is deliberate: `exe`, `docker`, `modal` and
133
+ `box` throw when handed a restricted policy, and `cloudflare` and the fetch
134
+ bridge accept one and silently ignore it. Withholding at the manager means the
135
+ throwing drivers never see a policy (their throws stay as their own last line of
136
+ defence) and the silently-dropping ones stop pretending.
137
+
138
+ The gap is never silent. `createSandboxManager` warns at composition, and again
139
+ each time a policy is withheld:
140
+
141
+ ```text
142
+ [sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "cloudflare" declares
143
+ egressControl: "none", so workspace ws_1 can reach ANY host on the internet …
144
+ ```
145
+
146
+ Pass `onEgressUnenforced` to route that into telemetry instead of `console.warn`
147
+ — it receives a structured `SandboxEgressUnenforcedEvent`. Overriding the sink
148
+ replaces the console warning, so only do it if the replacement is as visible.
149
+
150
+ **Reuse and resume reapply the policy.** A restricted policy only reaches a
151
+ provider as *creation* parameters, so a sandbox handed back rather than created
152
+ — reuse in `ensureHost`, resume in `resumeHost` — would otherwise run on the
153
+ policy in force when it was first created, possibly a wider one from an earlier
154
+ caller. Daytona's allow-list is mutable post-create
155
+ (`Sandbox.updateNetworkSettings`), so `daytona` reapplies the requested policy
156
+ on both paths. A client too old to expose that call makes the driver **refuse
157
+ the reuse**: a policy reported as applied but not in force is worse than no
158
+ sandbox. Requesting no containment leaves the existing policy alone rather than
159
+ clearing it — "not requested" is not "please unrestrict".
160
+
161
+ `driver.metadata.egressControl` is the machine-readable source of truth, and
162
+ `sandboxEgressDisposition(control, net)` is the pure predicate the manager uses,
163
+ so you can ask the same question before composing. One exception stays fail
164
+ closed: a hosts-only driver handed an address-only policy is refused
165
+ (`sandbox_egress_policy_unenforceable`) rather than degraded, because that
166
+ driver *does* enforce egress — it just cannot express that encoding.
122
167
 
123
168
  > SDK conformance: the Daytona secret API and Vercel network-policy transform
124
169
  > shapes follow the providers' official docs and are validated structurally
@@ -0,0 +1,47 @@
1
+ import type { SandboxCheckpointReference, SandboxEnsureResult, SandboxLease, SandboxLeaseStore, SandboxManagerInput, SandboxSnapshotManagerResult, SandboxTargetResult } from ".";
2
+ export type SandboxCheckpointRuntime = {
3
+ freeze: (policy: "drain" | "interrupt") => Promise<void>;
4
+ flush: () => Promise<void>;
5
+ scrub: () => Promise<void>;
6
+ resume: () => Promise<void>;
7
+ reconcile: (input: {
8
+ epoch: number;
9
+ checkpointId: string;
10
+ }) => Promise<void>;
11
+ };
12
+ export type SandboxCheckpointCaptureInput = {
13
+ runtime: SandboxCheckpointRuntime;
14
+ policy?: "drain" | "interrupt";
15
+ retentionExpiresAt?: number;
16
+ };
17
+ export type SandboxCheckpointRestoreInput = {
18
+ runtime: SandboxCheckpointRuntime;
19
+ checkpointId?: string;
20
+ ensure?: Omit<SandboxManagerInput, "homeRegion">;
21
+ };
22
+ export type SandboxCheckpointResult = {
23
+ status: "ready";
24
+ lease: SandboxLease;
25
+ checkpoint: SandboxCheckpointReference;
26
+ } | {
27
+ status: "provisioning";
28
+ lease: SandboxLease;
29
+ checkpoint: SandboxCheckpointReference;
30
+ retryAfterMs: number;
31
+ };
32
+ export declare function captureSandboxCheckpoint(input: {
33
+ workspaceId: string;
34
+ request: SandboxCheckpointCaptureInput;
35
+ leaseStore: SandboxLeaseStore;
36
+ target: (workspaceId: string) => Promise<SandboxTargetResult>;
37
+ snapshot: (workspaceId: string) => Promise<SandboxSnapshotManagerResult>;
38
+ now?: () => number;
39
+ checkpointId?: () => string;
40
+ }): Promise<SandboxCheckpointResult>;
41
+ export declare function restoreSandboxCheckpoint(input: {
42
+ workspaceId: string;
43
+ request: SandboxCheckpointRestoreInput;
44
+ leaseStore: SandboxLeaseStore;
45
+ ensure: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
46
+ now?: () => number;
47
+ }): Promise<SandboxCheckpointResult>;
@@ -0,0 +1,116 @@
1
+ // src/checkpoint-manager.ts
2
+ async function captureSandboxCheckpoint(input) {
3
+ const lease = await requireLease(input.leaseStore, input.workspaceId);
4
+ if (lease.status !== "ready") throw new Error("workspace_checkpoint_lease_not_ready");
5
+ const persistence = lease.persistence;
6
+ if (!persistence || persistence.capture === "none") throw new Error("workspace_checkpoint_unsupported");
7
+ const target = await input.target(input.workspaceId);
8
+ if (target.status !== "ready") throw new Error(target.reason);
9
+ await input.request.runtime.freeze(input.request.policy ?? "drain");
10
+ let capturedSource = false;
11
+ try {
12
+ await input.request.runtime.flush();
13
+ await input.request.runtime.scrub();
14
+ const captured = persistence.capture === "same-resource" ? { ok: true, snapshotId: target.driverResourceId ?? target.sandboxId } : await input.snapshot(input.workspaceId);
15
+ if (!captured.ok) throw new Error(captured.reason);
16
+ capturedSource = true;
17
+ const capturedAt = (input.now ?? Date.now)();
18
+ const checkpoint = {
19
+ id: input.checkpointId?.() ?? `cp_${lease.epoch}_${crypto.randomUUID()}`,
20
+ providerReference: captured.snapshotId,
21
+ sourceEpoch: lease.epoch,
22
+ capturedAt,
23
+ metadata: {
24
+ scope: persistence.capture,
25
+ sourceBehavior: requireCaptureSource(persistence.captureSource),
26
+ ...input.request.retentionExpiresAt === void 0 ? {} : { retentionExpiresAt: input.request.retentionExpiresAt },
27
+ restoreMount: requireRestoreMount(persistence.restoreMount)
28
+ }
29
+ };
30
+ const updated = await input.leaseStore.update(input.workspaceId, lease.epoch, {
31
+ checkpoint,
32
+ restore: null,
33
+ ...persistence.captureSource === "stopped" || persistence.captureSource === "deleted" ? { status: "stopped" } : {}
34
+ });
35
+ if (!updated) throw new Error("workspace_checkpoint_epoch_fenced");
36
+ return { status: "ready", lease: updated, checkpoint };
37
+ } finally {
38
+ if (!capturedSource || persistence.captureSource === "preserved") {
39
+ await input.request.runtime.resume();
40
+ }
41
+ }
42
+ }
43
+ async function restoreSandboxCheckpoint(input) {
44
+ const lease = await requireLease(input.leaseStore, input.workspaceId);
45
+ const checkpoint = lease.checkpoint;
46
+ if (!checkpoint || input.request.checkpointId && checkpoint.id !== input.request.checkpointId) {
47
+ throw new Error("workspace_checkpoint_not_found");
48
+ }
49
+ if (lease.restore?.state === "ready" && lease.restore.checkpointId === checkpoint.id) {
50
+ return { status: "ready", lease, checkpoint };
51
+ }
52
+ const requestedAt = lease.restore?.checkpointId === checkpoint.id ? lease.restore.requestedAt : (input.now ?? Date.now)();
53
+ const restoring = lease.restore?.checkpointId === checkpoint.id && lease.restore.state !== "pending" ? lease.restore : {
54
+ checkpointId: checkpoint.id,
55
+ sourceEpoch: checkpoint.sourceEpoch,
56
+ state: "restoring",
57
+ requestedAt,
58
+ startedAt: (input.now ?? Date.now)()
59
+ };
60
+ const staged = lease.status === "ready" && lease.epoch === checkpoint.sourceEpoch ? await input.leaseStore.update(input.workspaceId, lease.epoch, { status: "stopped", restore: restoring }) : await input.leaseStore.update(input.workspaceId, lease.epoch, { restore: restoring });
61
+ if (!staged) throw new Error("workspace_restore_epoch_fenced");
62
+ const result = await input.ensure(input.workspaceId, {
63
+ homeRegion: staged.homeRegion,
64
+ ...input.request.ensure,
65
+ bootSource: checkpoint.metadata.restoreMount === "same-resource" ? { kind: "default" } : { kind: "driver-snapshot", snapshotId: checkpoint.providerReference }
66
+ });
67
+ const current = await requireLease(input.leaseStore, input.workspaceId);
68
+ if (result.status === "provisioning") {
69
+ return { status: "provisioning", lease: current, checkpoint, retryAfterMs: result.retryAfterMs };
70
+ }
71
+ if (result.status !== "ready") {
72
+ const failedAt = (input.now ?? Date.now)();
73
+ await input.leaseStore.update(input.workspaceId, current.epoch, {
74
+ restore: {
75
+ checkpointId: checkpoint.id,
76
+ sourceEpoch: checkpoint.sourceEpoch,
77
+ state: "failed",
78
+ requestedAt,
79
+ startedAt: "startedAt" in restoring ? restoring.startedAt : void 0,
80
+ failedAt,
81
+ error: result.error ?? "workspace_restore_unavailable"
82
+ }
83
+ });
84
+ throw new Error(result.error ?? "workspace_restore_unavailable");
85
+ }
86
+ await input.request.runtime.reconcile({ epoch: result.epoch, checkpointId: checkpoint.id });
87
+ const ready = await input.leaseStore.update(input.workspaceId, result.epoch, {
88
+ restore: {
89
+ checkpointId: checkpoint.id,
90
+ sourceEpoch: checkpoint.sourceEpoch,
91
+ state: "ready",
92
+ requestedAt,
93
+ startedAt: "startedAt" in restoring && restoring.startedAt !== void 0 ? restoring.startedAt : (input.now ?? Date.now)(),
94
+ completedAt: (input.now ?? Date.now)()
95
+ }
96
+ });
97
+ if (!ready) throw new Error("workspace_restore_epoch_fenced");
98
+ return { status: "ready", lease: ready, checkpoint };
99
+ }
100
+ async function requireLease(store, workspaceId) {
101
+ const lease = await store.get(workspaceId);
102
+ if (!lease) throw new Error("workspace_checkpoint_lease_missing");
103
+ return lease;
104
+ }
105
+ function requireCaptureSource(value) {
106
+ if (value === "preserved" || value === "stopped" || value === "deleted") return value;
107
+ throw new Error("workspace_checkpoint_capabilities_invalid");
108
+ }
109
+ function requireRestoreMount(value) {
110
+ if (value === "same-resource" || value === "copy-on-write" || value === "new-resource") return value;
111
+ throw new Error("workspace_checkpoint_capabilities_invalid");
112
+ }
113
+ export {
114
+ captureSandboxCheckpoint,
115
+ restoreSandboxCheckpoint
116
+ };
@@ -3,3 +3,14 @@
3
3
  * Daytona currently accepts at most 10 entries.
4
4
  */
5
5
  export declare function formatDaytonaAllowList(cidrs: string[]): string;
6
+ /**
7
+ * Format hostnames for Daytona SDK `domainAllowList` parameter.
8
+ *
9
+ * Deliberately does NOT truncate, unlike the CIDR formatter above. Silently
10
+ * dropping entries from an egress allowlist is the worst of both worlds: the
11
+ * sandbox stays contained, but it loses reachability the caller believed it
12
+ * granted and nothing anywhere says so. Daytona documents no cap on
13
+ * `domainAllowList`; if one exists, an over-long list fails the create call
14
+ * loudly, which is the outcome we want.
15
+ */
16
+ export declare function formatDaytonaDomainAllowList(hosts: string[]): string;
@@ -2,6 +2,10 @@
2
2
  function formatDaytonaAllowList(cidrs) {
3
3
  return cidrs.slice(0, 10).join(",");
4
4
  }
5
+ function formatDaytonaDomainAllowList(hosts) {
6
+ return [...new Set(hosts)].join(",");
7
+ }
5
8
  export {
6
- formatDaytonaAllowList
9
+ formatDaytonaAllowList,
10
+ formatDaytonaDomainAllowList
7
11
  };
@@ -1,65 +1,18 @@
1
- import type { SandboxDriverMetadata } from "./index";
2
- export declare const sandboxDriverIds: readonly ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
3
- export type SandboxDriverID = (typeof sandboxDriverIds)[number];
4
- export type SandboxDriverAuth = {
5
- daytona?: {
6
- api_key?: string;
7
- };
8
- modal?: {
9
- token_id?: string;
10
- token_secret?: string;
11
- };
12
- vercel?: {
13
- access_token?: string;
14
- team_id?: string;
15
- project_id?: string;
16
- };
17
- cloudflare?: {
18
- api_token?: string;
19
- worker_url?: string;
20
- };
21
- box?: {
22
- api_key?: string;
23
- };
24
- docker?: {
25
- image?: string;
26
- };
27
- };
28
- export type SandboxDriverConfig = {
29
- default_driver?: SandboxDriverID;
30
- auth?: SandboxDriverAuth;
31
- };
1
+ import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./index";
2
+ import { defaultSandboxDriverID, listSandboxDrivers, sandboxDriverId, type SandboxDriverAuth, type SandboxDriverConfig, type SandboxDriverEnv, type SandboxDriverID } from "@claxedo/sandbox-contract";
3
+ export { defaultSandboxDriverID, listSandboxDrivers, sandboxDriverId };
32
4
  export type SandboxDriverCatalogEntry = {
33
5
  id: SandboxDriverID;
34
6
  label: string;
35
7
  metadata: SandboxDriverMetadata;
36
- credentialFields: Array<{
8
+ credentialFields: ReadonlyArray<{
37
9
  key: string;
38
10
  label: string;
39
11
  secret?: boolean;
40
12
  }>;
41
13
  };
14
+ export { validateSandboxPersistenceCapabilities };
42
15
  export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
43
- export declare function isSandboxDriverID(input: string | undefined): input is SandboxDriverID;
44
- export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel" | undefined;
45
- export declare function defaultSandboxDriverID(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): SandboxDriverID;
46
16
  export declare function sandboxDriverAuth<T extends SandboxDriverID>(cfg: SandboxDriverConfig | undefined, id: T, env?: SandboxDriverEnv): SandboxDriverAuth[T] | undefined;
47
17
  export declare function hasSandboxDriverAuth(cfg: SandboxDriverConfig | undefined, id: SandboxDriverID, env?: SandboxDriverEnv): boolean;
48
18
  export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
49
- export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv, managedDriverIds?: ReadonlySet<string>): {
50
- default_driver: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
51
- drivers: {
52
- id: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
53
- label: string;
54
- fields: {
55
- key: string;
56
- label: string;
57
- secret?: boolean;
58
- }[];
59
- configured: boolean;
60
- source: "config" | "managed" | "none";
61
- default: boolean;
62
- }[];
63
- };
64
- type SandboxDriverEnv = Record<string, string | undefined>;
65
- export {};