openlattice-modal 0.0.3
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/dist/config.d.ts +8 -0
- package/dist/config.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/modal-provider.d.ts +26 -0
- package/dist/modal-provider.js +363 -0
- package/package.json +37 -0
- package/src/config.ts +8 -0
- package/src/index.ts +2 -0
- package/src/modal-provider.ts +435 -0
- package/tests/conformance.test.ts +24 -0
- package/tests/integration.test.ts +131 -0
- package/tests/modal-provider.test.ts +461 -0
- package/tsconfig.json +16 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ModalProviderConfig {
|
|
2
|
+
/** Modal token ID. Default: reads MODAL_TOKEN_ID env var. */
|
|
3
|
+
tokenId?: string;
|
|
4
|
+
/** Modal token secret. Default: reads MODAL_TOKEN_SECRET env var. */
|
|
5
|
+
tokenSecret?: string;
|
|
6
|
+
/** Modal app name to use. Default: "openlattice". */
|
|
7
|
+
appName?: string;
|
|
8
|
+
}
|
package/dist/config.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ModalProvider = void 0;
|
|
4
|
+
var modal_provider_1 = require("./modal-provider");
|
|
5
|
+
Object.defineProperty(exports, "ModalProvider", { enumerable: true, get: function () { return modal_provider_1.ModalProvider; } });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComputeProvider, ComputeSpec, ExecOpts, ExecResult, ExtensionMap, HealthStatus, ProviderCapabilities, ProviderNode, ProviderNodeStatus, SnapshotRef } from "openlattice";
|
|
2
|
+
import type { ModalProviderConfig } from "./config";
|
|
3
|
+
export declare class ModalProvider implements ComputeProvider {
|
|
4
|
+
readonly name = "modal";
|
|
5
|
+
readonly capabilities: ProviderCapabilities;
|
|
6
|
+
private readonly config;
|
|
7
|
+
private client;
|
|
8
|
+
private app;
|
|
9
|
+
private readonly sandboxes;
|
|
10
|
+
constructor(config?: ModalProviderConfig);
|
|
11
|
+
provision(spec: ComputeSpec): Promise<ProviderNode>;
|
|
12
|
+
exec(externalId: string, command: string[], opts?: ExecOpts): Promise<ExecResult>;
|
|
13
|
+
destroy(externalId: string): Promise<void>;
|
|
14
|
+
inspect(externalId: string): Promise<ProviderNodeStatus>;
|
|
15
|
+
snapshot(externalId: string): Promise<SnapshotRef>;
|
|
16
|
+
restore(snapshotRef: SnapshotRef, spec?: Partial<ComputeSpec>): Promise<ProviderNode>;
|
|
17
|
+
healthCheck(): Promise<HealthStatus>;
|
|
18
|
+
getExtension<K extends keyof ExtensionMap>(externalId: string, extension: K): ExtensionMap[K] | undefined;
|
|
19
|
+
private tailscaleUp;
|
|
20
|
+
private getClient;
|
|
21
|
+
private getApp;
|
|
22
|
+
private resolveImage;
|
|
23
|
+
private getSandbox;
|
|
24
|
+
private createFileExtension;
|
|
25
|
+
private createNetworkExtension;
|
|
26
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ModalProvider = void 0;
|
|
37
|
+
class ModalProvider {
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.name = "modal";
|
|
40
|
+
this.capabilities = {
|
|
41
|
+
restart: false,
|
|
42
|
+
pause: false,
|
|
43
|
+
snapshot: true,
|
|
44
|
+
gpu: true,
|
|
45
|
+
logs: false,
|
|
46
|
+
tailscale: true, // userspace networking mode, no special perms needed
|
|
47
|
+
coldStartMs: 2000,
|
|
48
|
+
maxConcurrent: 0,
|
|
49
|
+
architectures: ["x86_64"],
|
|
50
|
+
persistentStorage: false,
|
|
51
|
+
constraints: {
|
|
52
|
+
maxLifetimeSeconds: 86400,
|
|
53
|
+
gpuTypes: ["T4", "A10G", "A100", "H100", "H200", "L4", "L40S", "B200"],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
this.client = null;
|
|
57
|
+
this.app = null;
|
|
58
|
+
this.sandboxes = new Map();
|
|
59
|
+
this.config = config;
|
|
60
|
+
}
|
|
61
|
+
// ── Required methods ────────────────────────────────────────────
|
|
62
|
+
async provision(spec) {
|
|
63
|
+
const client = await this.getClient();
|
|
64
|
+
const app = await this.getApp(client);
|
|
65
|
+
const image = this.resolveImage(client, spec.runtime.image);
|
|
66
|
+
const createOpts = {};
|
|
67
|
+
if (spec.gpu?.type) {
|
|
68
|
+
createOpts.gpu = spec.gpu.type;
|
|
69
|
+
}
|
|
70
|
+
else if (spec.gpu?.count) {
|
|
71
|
+
createOpts.gpu = "T4"; // default GPU
|
|
72
|
+
}
|
|
73
|
+
if (spec.duration?.maxSeconds) {
|
|
74
|
+
createOpts.timeout = spec.duration.maxSeconds;
|
|
75
|
+
}
|
|
76
|
+
if (spec.runtime.workdir) {
|
|
77
|
+
createOpts.workdir = spec.runtime.workdir;
|
|
78
|
+
}
|
|
79
|
+
if (spec.runtime.env) {
|
|
80
|
+
createOpts.environmentVariables = spec.runtime.env;
|
|
81
|
+
}
|
|
82
|
+
const sandbox = await client.sandboxes.create(app, image, createOpts);
|
|
83
|
+
this.sandboxes.set(sandbox.sandboxId, sandbox);
|
|
84
|
+
// Run initial command if specified
|
|
85
|
+
if (spec.runtime.command && spec.runtime.command.length > 0) {
|
|
86
|
+
await sandbox.exec(spec.runtime.command, {
|
|
87
|
+
stdout: "pipe",
|
|
88
|
+
stderr: "pipe",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Join Tailscale network if auth key provided
|
|
92
|
+
if (spec.network?.tailscaleAuthKey) {
|
|
93
|
+
await this.tailscaleUp(sandbox, spec.network.tailscaleAuthKey);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
externalId: sandbox.sandboxId,
|
|
97
|
+
endpoints: [],
|
|
98
|
+
metadata: { appName: this.config.appName ?? "openlattice" },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async exec(externalId, command, opts) {
|
|
102
|
+
const sandbox = await this.getSandbox(externalId);
|
|
103
|
+
// Build command with cwd and env support
|
|
104
|
+
let cmd = command;
|
|
105
|
+
if (opts?.cwd || opts?.env) {
|
|
106
|
+
const parts = [];
|
|
107
|
+
if (opts.cwd) {
|
|
108
|
+
parts.push(`cd ${opts.cwd}`);
|
|
109
|
+
}
|
|
110
|
+
if (opts.env) {
|
|
111
|
+
for (const [k, v] of Object.entries(opts.env)) {
|
|
112
|
+
parts.push(`export ${k}='${v.replace(/'/g, "'\\''")}'`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
parts.push(command.join(" "));
|
|
116
|
+
cmd = ["sh", "-c", parts.join(" && ")];
|
|
117
|
+
}
|
|
118
|
+
const execOpts = {
|
|
119
|
+
stdout: "pipe",
|
|
120
|
+
stderr: "pipe",
|
|
121
|
+
};
|
|
122
|
+
if (opts?.timeoutMs) {
|
|
123
|
+
execOpts.timeout = Math.floor(opts.timeoutMs / 1000);
|
|
124
|
+
}
|
|
125
|
+
const proc = await sandbox.exec(cmd, execOpts);
|
|
126
|
+
const [stdout, stderr] = await Promise.all([
|
|
127
|
+
proc.stdout.readText(),
|
|
128
|
+
proc.stderr.readText(),
|
|
129
|
+
]);
|
|
130
|
+
const exitCode = await proc.wait();
|
|
131
|
+
opts?.onStdout?.(stdout);
|
|
132
|
+
opts?.onStderr?.(stderr);
|
|
133
|
+
return { exitCode, stdout, stderr };
|
|
134
|
+
}
|
|
135
|
+
async destroy(externalId) {
|
|
136
|
+
const sandbox = this.sandboxes.get(externalId);
|
|
137
|
+
if (!sandbox) {
|
|
138
|
+
// Try to reconnect and terminate
|
|
139
|
+
try {
|
|
140
|
+
const client = await this.getClient();
|
|
141
|
+
const sb = await client.sandboxes.fromId(externalId);
|
|
142
|
+
await sb.terminate();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Already terminated or not found — idempotent
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await sandbox.terminate();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Already terminated — idempotent
|
|
154
|
+
}
|
|
155
|
+
this.sandboxes.delete(externalId);
|
|
156
|
+
}
|
|
157
|
+
async inspect(externalId) {
|
|
158
|
+
try {
|
|
159
|
+
const client = await this.getClient();
|
|
160
|
+
const sandbox = await client.sandboxes.fromId(externalId);
|
|
161
|
+
// If we can reconnect, it's running
|
|
162
|
+
this.sandboxes.set(externalId, sandbox);
|
|
163
|
+
return { status: "running" };
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
this.sandboxes.delete(externalId);
|
|
167
|
+
return { status: "terminated" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ── Optional: snapshot / restore ──────────────────────────────
|
|
171
|
+
async snapshot(externalId) {
|
|
172
|
+
const sandbox = await this.getSandbox(externalId);
|
|
173
|
+
const image = await sandbox.snapshotFilesystem();
|
|
174
|
+
return {
|
|
175
|
+
provider: "modal",
|
|
176
|
+
externalId: image.imageId,
|
|
177
|
+
createdAt: new Date(),
|
|
178
|
+
type: "filesystem",
|
|
179
|
+
metadata: { originalSandboxId: externalId },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async restore(snapshotRef, spec) {
|
|
183
|
+
const client = await this.getClient();
|
|
184
|
+
const app = await this.getApp(client);
|
|
185
|
+
// The snapshotRef.externalId is the Modal image ID
|
|
186
|
+
const image = { imageId: snapshotRef.externalId };
|
|
187
|
+
const createOpts = {};
|
|
188
|
+
if (spec?.gpu?.type) {
|
|
189
|
+
createOpts.gpu = spec.gpu.type;
|
|
190
|
+
}
|
|
191
|
+
if (spec?.duration?.maxSeconds) {
|
|
192
|
+
createOpts.timeout = spec.duration.maxSeconds;
|
|
193
|
+
}
|
|
194
|
+
const sandbox = await client.sandboxes.create(app, image, createOpts);
|
|
195
|
+
this.sandboxes.set(sandbox.sandboxId, sandbox);
|
|
196
|
+
return {
|
|
197
|
+
externalId: sandbox.sandboxId,
|
|
198
|
+
endpoints: [],
|
|
199
|
+
metadata: {
|
|
200
|
+
appName: this.config.appName ?? "openlattice",
|
|
201
|
+
restoredFrom: snapshotRef.externalId,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// ── Optional: healthCheck ─────────────────────────────────────
|
|
206
|
+
async healthCheck() {
|
|
207
|
+
const start = Date.now();
|
|
208
|
+
try {
|
|
209
|
+
const client = await this.getClient();
|
|
210
|
+
await client.apps.fromName(this.config.appName ?? "openlattice", {
|
|
211
|
+
createIfMissing: true,
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
healthy: true,
|
|
215
|
+
latencyMs: Date.now() - start,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
healthy: false,
|
|
221
|
+
latencyMs: Date.now() - start,
|
|
222
|
+
message: `[modal] service unreachable: ${err instanceof Error ? err.message : String(err)}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ── Optional: extensions ──────────────────────────────────────
|
|
227
|
+
getExtension(externalId, extension) {
|
|
228
|
+
if (extension === "files") {
|
|
229
|
+
return this.createFileExtension(externalId);
|
|
230
|
+
}
|
|
231
|
+
if (extension === "network") {
|
|
232
|
+
return this.createNetworkExtension(externalId);
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
// ── Private helpers ───────────────────────────────────────────
|
|
237
|
+
async tailscaleUp(sandbox, authKey) {
|
|
238
|
+
const proc = await sandbox.exec([
|
|
239
|
+
"sh",
|
|
240
|
+
"-c",
|
|
241
|
+
`pgrep tailscaled >/dev/null 2>&1 || tailscaled --state=/var/lib/tailscale/tailscaled.state & sleep 1 && tailscale up --authkey=${authKey}`,
|
|
242
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
243
|
+
const stderr = await proc.stderr.readText();
|
|
244
|
+
const exitCode = await proc.wait();
|
|
245
|
+
if (exitCode !== 0) {
|
|
246
|
+
throw new Error(`[modal] tailscale up failed: ${stderr.trim()}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async getClient() {
|
|
250
|
+
if (this.client)
|
|
251
|
+
return this.client;
|
|
252
|
+
// Set Modal credential env vars if provided in config
|
|
253
|
+
if (this.config.tokenId) {
|
|
254
|
+
process.env.MODAL_TOKEN_ID = this.config.tokenId;
|
|
255
|
+
}
|
|
256
|
+
if (this.config.tokenSecret) {
|
|
257
|
+
process.env.MODAL_TOKEN_SECRET = this.config.tokenSecret;
|
|
258
|
+
}
|
|
259
|
+
// Dynamic import to handle ESM/CJS
|
|
260
|
+
const modal = await Promise.resolve().then(() => __importStar(require("modal")));
|
|
261
|
+
const ModalClientClass = modal.ModalClient ?? modal.default?.ModalClient;
|
|
262
|
+
if (!ModalClientClass) {
|
|
263
|
+
throw new Error("[modal] could not import ModalClient from modal package");
|
|
264
|
+
}
|
|
265
|
+
this.client = new ModalClientClass();
|
|
266
|
+
return this.client;
|
|
267
|
+
}
|
|
268
|
+
async getApp(client) {
|
|
269
|
+
if (this.app)
|
|
270
|
+
return this.app;
|
|
271
|
+
this.app = await client.apps.fromName(this.config.appName ?? "openlattice", { createIfMissing: true });
|
|
272
|
+
return this.app;
|
|
273
|
+
}
|
|
274
|
+
resolveImage(client, image) {
|
|
275
|
+
// If it looks like an OCI reference (contains / or :), use fromRegistry
|
|
276
|
+
if (image.includes("/") || image.includes(":")) {
|
|
277
|
+
return client.images.fromRegistry(image);
|
|
278
|
+
}
|
|
279
|
+
// Otherwise assume it's a Modal image ID
|
|
280
|
+
return { imageId: image };
|
|
281
|
+
}
|
|
282
|
+
async getSandbox(externalId) {
|
|
283
|
+
const cached = this.sandboxes.get(externalId);
|
|
284
|
+
if (cached)
|
|
285
|
+
return cached;
|
|
286
|
+
const client = await this.getClient();
|
|
287
|
+
const sandbox = await client.sandboxes.fromId(externalId);
|
|
288
|
+
this.sandboxes.set(externalId, sandbox);
|
|
289
|
+
return sandbox;
|
|
290
|
+
}
|
|
291
|
+
createFileExtension(externalId) {
|
|
292
|
+
const provider = this;
|
|
293
|
+
return {
|
|
294
|
+
async read(path) {
|
|
295
|
+
const sb = await provider.getSandbox(externalId);
|
|
296
|
+
const proc = await sb.exec(["cat", path], { stdout: "pipe", stderr: "pipe" });
|
|
297
|
+
const content = await proc.stdout.readText();
|
|
298
|
+
await proc.wait();
|
|
299
|
+
return content;
|
|
300
|
+
},
|
|
301
|
+
async write(filePath, content) {
|
|
302
|
+
const sb = await provider.getSandbox(externalId);
|
|
303
|
+
const safePath = filePath.replace(/'/g, "'\\''");
|
|
304
|
+
if (Buffer.isBuffer(content)) {
|
|
305
|
+
// Binary content: base64 encode, decode on remote
|
|
306
|
+
const b64 = content.toString("base64");
|
|
307
|
+
const proc = await sb.exec(["sh", "-c", `printf '%s' '${b64}' | base64 -d > '${safePath}'`], { stdout: "pipe", stderr: "pipe" });
|
|
308
|
+
await proc.wait();
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Text content: write via printf with proper escaping
|
|
312
|
+
const escaped = content.replace(/\\/g, "\\\\").replace(/'/g, "'\\''");
|
|
313
|
+
const proc = await sb.exec(["sh", "-c", `printf '%s' '${escaped}' > '${safePath}'`], { stdout: "pipe", stderr: "pipe" });
|
|
314
|
+
await proc.wait();
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
async list(dirPath) {
|
|
318
|
+
const sb = await provider.getSandbox(externalId);
|
|
319
|
+
const proc = await sb.exec(["ls", "-1apL", dirPath], { stdout: "pipe", stderr: "pipe" });
|
|
320
|
+
const output = await proc.stdout.readText();
|
|
321
|
+
await proc.wait();
|
|
322
|
+
return output
|
|
323
|
+
.split("\n")
|
|
324
|
+
.filter((l) => l.length > 0 && l !== "./" && l !== "../")
|
|
325
|
+
.map((name) => {
|
|
326
|
+
const isDir = name.endsWith("/");
|
|
327
|
+
const cleanName = isDir ? name.slice(0, -1) : name;
|
|
328
|
+
return {
|
|
329
|
+
name: cleanName,
|
|
330
|
+
path: `${dirPath}/${cleanName}`.replace(/\/\//g, "/"),
|
|
331
|
+
type: isDir ? "directory" : "file",
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
async remove(path) {
|
|
336
|
+
const sb = await provider.getSandbox(externalId);
|
|
337
|
+
const proc = await sb.exec(["rm", "-rf", path], { stdout: "pipe", stderr: "pipe" });
|
|
338
|
+
await proc.wait();
|
|
339
|
+
},
|
|
340
|
+
async mkdir(path) {
|
|
341
|
+
const sb = await provider.getSandbox(externalId);
|
|
342
|
+
const proc = await sb.exec(["mkdir", "-p", path], { stdout: "pipe", stderr: "pipe" });
|
|
343
|
+
await proc.wait();
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
createNetworkExtension(externalId) {
|
|
348
|
+
const provider = this;
|
|
349
|
+
return {
|
|
350
|
+
async getUrl(port) {
|
|
351
|
+
const sb = await provider.getSandbox(externalId);
|
|
352
|
+
// Modal provides tunnel URLs via sandbox
|
|
353
|
+
const tunnelUrl = sb.tunnelUrl ?? sb.getTunnelUrl;
|
|
354
|
+
if (typeof tunnelUrl === "function") {
|
|
355
|
+
return tunnelUrl(port);
|
|
356
|
+
}
|
|
357
|
+
// Fallback: construct from sandbox ID
|
|
358
|
+
return `https://${externalId}--${port}.modal.run`;
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
exports.ModalProvider = ModalProvider;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openlattice-modal",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Modal compute provider for OpenLattice",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:integration": "vitest run tests/integration.test.ts tests/conformance.test.ts"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"compute",
|
|
20
|
+
"modal",
|
|
21
|
+
"openlattice",
|
|
22
|
+
"provider"
|
|
23
|
+
],
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"openlattice": "^0.0.3"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"modal": "^0.6.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.2.2",
|
|
33
|
+
"openlattice": "^0.0.3",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ModalProviderConfig {
|
|
2
|
+
/** Modal token ID. Default: reads MODAL_TOKEN_ID env var. */
|
|
3
|
+
tokenId?: string;
|
|
4
|
+
/** Modal token secret. Default: reads MODAL_TOKEN_SECRET env var. */
|
|
5
|
+
tokenSecret?: string;
|
|
6
|
+
/** Modal app name to use. Default: "openlattice". */
|
|
7
|
+
appName?: string;
|
|
8
|
+
}
|
package/src/index.ts
ADDED