@vivix-labs/material-library 0.0.0-stage → 1.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import { MaterialConfig } from './shared.js';
3
+ export { Config } from './shared.js';
4
+ import { ClientContext } from 'vivix-studio/sdk/client';
5
+ import 'vivix-studio/sdk/shared';
6
+
7
+ declare function MaterialProperties({ record }: {
8
+ record: any;
9
+ }): React.JSX.Element;
10
+
11
+ declare const dependencies: (config: MaterialConfig) => string[];
12
+ declare function activate(client: ClientContext<MaterialConfig>): void;
13
+
14
+ export { MaterialProperties, activate as default, dependencies };
package/dist/client.js ADDED
@@ -0,0 +1,94 @@
1
+ // packages/material-library/src/ui/panels.tsx
2
+ import * as React from "react";
3
+ function ImportToProject(props) {
4
+ const { form, setForm, projects, save } = props;
5
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Target project", /* @__PURE__ */ React.createElement(
6
+ "select",
7
+ {
8
+ value: form.targetProjectId,
9
+ onChange: (e) => setForm({ ...form, targetProjectId: e.target.value })
10
+ },
11
+ /* @__PURE__ */ React.createElement("option", { value: "" }, "Choose a project"),
12
+ projects.map((p) => /* @__PURE__ */ React.createElement("option", { key: p.objectId, value: p.objectId }, p.data.name))
13
+ )), /* @__PURE__ */ React.createElement(
14
+ "button",
15
+ {
16
+ className: "primary",
17
+ disabled: !form.targetProjectId,
18
+ onClick: () => void save("assets.importMaterial", {
19
+ ref: form.ref,
20
+ targetProjectId: form.targetProjectId
21
+ })
22
+ },
23
+ "Copy into project"
24
+ ));
25
+ }
26
+
27
+ // packages/material-library/src/client.tsx
28
+ import * as React2 from "react";
29
+
30
+ // packages/material-library/src/shared.ts
31
+ import { z } from "vivix-studio/sdk/shared";
32
+ var Config = z.discriminatedUnion("role", [
33
+ z.object({
34
+ role: z.literal("service"),
35
+ dataInstance: z.string().min(1).default("data"),
36
+ serviceInstance: z.string().min(1).default("library")
37
+ }).strict(),
38
+ z.object({
39
+ role: z.literal("client"),
40
+ serviceInstance: z.string().min(1).default("user:library")
41
+ }).strict()
42
+ ]);
43
+ var materialFields = z.object({
44
+ name: z.string().trim().min(1).max(200),
45
+ kind: z.enum(["text", "image", "video", "audio", "file"]),
46
+ tags: z.array(z.string().trim().min(1).max(64)).max(50).transform((x) => [...new Set(x)]).default([]),
47
+ category: z.enum(["character", "scene", "style", "sound", "reference", "other"]).default("other"),
48
+ description: z.string().max(4e3).default(""),
49
+ origin: z.enum(["import", "collected"]),
50
+ sourceRef: z.object({ storeId: z.uuid(), objectId: z.uuid(), versionId: z.uuid() }).strict().nullable().default(null),
51
+ sourceLabel: z.object({
52
+ projectName: z.string(),
53
+ objectName: z.string(),
54
+ versionNumber: z.number().int().positive()
55
+ }).strict().nullable().default(null),
56
+ lifecycle: z.enum(["ready", "archived"]).default("ready"),
57
+ recommendedVersionId: z.uuid(),
58
+ licenseNote: z.string().max(4e3).default("")
59
+ }).strict();
60
+ var materialData = materialFields.refine(
61
+ (v) => v.origin !== "collected" || v.sourceRef !== null,
62
+ "Collected materials require a fixed source."
63
+ );
64
+
65
+ // packages/material-library/src/client.tsx
66
+ function MaterialProperties({ record }) {
67
+ return /* @__PURE__ */ React2.createElement("dl", null, /* @__PURE__ */ React2.createElement("dt", null, "Category"), /* @__PURE__ */ React2.createElement("dd", null, record.data.category), /* @__PURE__ */ React2.createElement("dt", null, "Tags"), /* @__PURE__ */ React2.createElement("dd", null, record.data.tags.map((t) => /* @__PURE__ */ React2.createElement("span", { key: t, className: "tag" }, t))), /* @__PURE__ */ React2.createElement("dt", null, "Source"), /* @__PURE__ */ React2.createElement("dd", null, record.data.sourceLabel ? `${record.data.sourceLabel.projectName} / ${record.data.sourceLabel.objectName} / v${record.data.sourceLabel.versionNumber}` : record.data.origin), /* @__PURE__ */ React2.createElement("dt", null, "Recommended version"), /* @__PURE__ */ React2.createElement("dd", null, "v", record.versions?.find((v) => v.versionId === record.data.recommendedVersionId)?.versionNumber ?? "\u2014"), /* @__PURE__ */ React2.createElement("dt", null, "Uses"), /* @__PURE__ */ React2.createElement("dd", null, record.usageCount ?? "Query usage"), /* @__PURE__ */ React2.createElement("dt", null, "License"), /* @__PURE__ */ React2.createElement("dd", null, record.data.licenseNote || "Unspecified"));
68
+ }
69
+ var dependencies = (config) => config.role === "client" ? [config.serviceInstance] : [];
70
+ function activate(client) {
71
+ client.provide({ serviceInstance: client.config.serviceInstance });
72
+ let active = true;
73
+ client.registerUI({
74
+ id: "library.import-form",
75
+ title: "Import material",
76
+ kind: "component",
77
+ component: (props) => active ? /* @__PURE__ */ React2.createElement(ImportToProject, { ...props }) : null
78
+ });
79
+ client.registerUI({
80
+ id: "library.properties",
81
+ title: "Material properties",
82
+ kind: "component",
83
+ component: (props) => active ? /* @__PURE__ */ React2.createElement(MaterialProperties, { ...props }) : null
84
+ });
85
+ client.onDispose(() => {
86
+ active = false;
87
+ });
88
+ }
89
+ export {
90
+ Config,
91
+ MaterialProperties,
92
+ activate as default,
93
+ dependencies
94
+ };
@@ -0,0 +1,8 @@
1
+ import { ServerContext } from 'vivix-studio/sdk/server';
2
+ import { z } from 'vivix-studio/sdk/shared';
3
+ import { Config } from './shared.js';
4
+
5
+ declare const dependencies: (config: z.infer<typeof Config>) => string[];
6
+ declare function activate(ctx: ServerContext<z.infer<typeof Config>>): void;
7
+
8
+ export { Config, activate as default, dependencies };
package/dist/server.js ADDED
@@ -0,0 +1,568 @@
1
+ // packages/material-library/src/server.ts
2
+ import { defineOperation, PluginError } from "vivix-studio/sdk/server";
3
+ import { z as z2 } from "vivix-studio/sdk/shared";
4
+ import { createHash } from "crypto";
5
+ import { createReadStream } from "fs";
6
+
7
+ // packages/material-library/src/shared.ts
8
+ import { z } from "vivix-studio/sdk/shared";
9
+ var Config = z.discriminatedUnion("role", [
10
+ z.object({
11
+ role: z.literal("service"),
12
+ dataInstance: z.string().min(1).default("data"),
13
+ serviceInstance: z.string().min(1).default("library")
14
+ }).strict(),
15
+ z.object({
16
+ role: z.literal("client"),
17
+ serviceInstance: z.string().min(1).default("user:library")
18
+ }).strict()
19
+ ]);
20
+ var materialFields = z.object({
21
+ name: z.string().trim().min(1).max(200),
22
+ kind: z.enum(["text", "image", "video", "audio", "file"]),
23
+ tags: z.array(z.string().trim().min(1).max(64)).max(50).transform((x) => [...new Set(x)]).default([]),
24
+ category: z.enum(["character", "scene", "style", "sound", "reference", "other"]).default("other"),
25
+ description: z.string().max(4e3).default(""),
26
+ origin: z.enum(["import", "collected"]),
27
+ sourceRef: z.object({ storeId: z.uuid(), objectId: z.uuid(), versionId: z.uuid() }).strict().nullable().default(null),
28
+ sourceLabel: z.object({
29
+ projectName: z.string(),
30
+ objectName: z.string(),
31
+ versionNumber: z.number().int().positive()
32
+ }).strict().nullable().default(null),
33
+ lifecycle: z.enum(["ready", "archived"]).default("ready"),
34
+ recommendedVersionId: z.uuid(),
35
+ licenseNote: z.string().max(4e3).default("")
36
+ }).strict();
37
+ var materialData = materialFields.refine(
38
+ (v) => v.origin !== "collected" || v.sourceRef !== null,
39
+ "Collected materials require a fixed source."
40
+ );
41
+
42
+ // packages/material-library/src/server.ts
43
+ var dependencies = (config) => config.role === "service" ? [config.dataInstance] : [];
44
+ var ownerPackage = "@vivix/material-library";
45
+ var packageVersion = "0.1.22-ecommerce.20261110.1";
46
+ var id = z2.uuid();
47
+ var revision = z2.number().int().positive();
48
+ var ref = z2.object({ storeId: id, objectId: id, versionId: id }).strict();
49
+ var usageSchema = z2.object({
50
+ materialRef: ref,
51
+ projectId: z2.string().min(1).max(128),
52
+ targetAssetId: id,
53
+ targetVersionId: id,
54
+ name: z2.string().max(200).default(""),
55
+ active: z2.boolean().default(true)
56
+ }).strict();
57
+ function fail(code, message) {
58
+ throw new PluginError(code, message, "Review the material and the current project, then retry.");
59
+ }
60
+ var materialDefinition = {
61
+ typeId: "library.material",
62
+ schemaVersion: 1,
63
+ scopeKinds: ["user"],
64
+ validate: (value) => materialData.parse(value),
65
+ validateUpdate: (before, after) => {
66
+ if (before.kind !== after.kind || before.origin !== after.origin || ["storeId", "objectId", "versionId"].some(
67
+ (key) => before.sourceRef?.[key] !== after.sourceRef?.[key]
68
+ ))
69
+ fail("IMMUTABLE_FIELD", "Material identity and source are immutable.");
70
+ }
71
+ };
72
+ var usageDefinition = {
73
+ typeId: "library.usage",
74
+ schemaVersion: 1,
75
+ scopeKinds: ["user"],
76
+ validate: (value) => usageSchema.parse(value),
77
+ validateUpdate: (before, after) => {
78
+ if (before.projectId !== after.projectId || before.targetAssetId !== after.targetAssetId || before.targetVersionId !== after.targetVersionId || ["storeId", "objectId", "versionId"].some(
79
+ (key) => before.materialRef[key] !== after.materialRef[key]
80
+ ))
81
+ fail("IMMUTABLE_FIELD", "Usage identities refer to fixed source and target versions.");
82
+ }
83
+ };
84
+ var operationDescriptions = {
85
+ "documents.list": "List persisted extension documents in the bound scope. Does not create content.",
86
+ "documents.create": "Create a persisted extension document with the original write request key.",
87
+ "documents.update": "Save document content only at its observed revision; preserve the draft on conflict.",
88
+ "documents.delete": "Delete the specified extension document at its observed revision.",
89
+ "files.resolveLocation": "Resolve an exact immutable version and optional file ID to a readable local location and preview URL. Does not render or export.",
90
+ "files.openLocation": "Open the containing directory of an exact readable immutable version. Does not modify its bytes.",
91
+ "metadata.patch": "Update only the named object metadata using its observed revision and a stable write request key.",
92
+ "metadata.keys": "List metadata keys available in this binding for building actual queries and views.",
93
+ "library.list": "List library materials with filters and pagination; materials are distinct from project asset candidates.",
94
+ "library.get": "Read one material and its immutable saved versions before choosing a fixed reference.",
95
+ "library.import": "Create a library material from validated staged file IDs and retain its provenance.",
96
+ "library.importVersion": "Append an immutable material version from validated staged files at the observed revision.",
97
+ "library.update": "Update material name, tags, or settings at the observed revision while preserving saved bytes.",
98
+ "library.archive": "Archive the material without rewriting existing project copies or immutable references.",
99
+ "library.restore": "Restore an archived material without generating new content.",
100
+ "library.usage": "List actual project uses of the specified material/version. Continue paging until nextOffset is null.",
101
+ "library.cleanupImpact": "Read references and uses that protect a material before permanent purge.",
102
+ "library.purge": "Permanently purge eligible material content after impact checks and explicit confirmation."
103
+ };
104
+ function activate(ctx) {
105
+ if (ctx.config.role === "client") {
106
+ if (ctx.scope?.kind !== "project")
107
+ fail("SCOPE_MISMATCH", "Library clients belong to projects.");
108
+ if (!ctx.config.serviceInstance.startsWith("user:"))
109
+ fail("INVALID_CONFIG", "Library clients require an explicit user service instance.");
110
+ return;
111
+ }
112
+ if (ctx.scope?.kind !== "user") fail("SCOPE_MISMATCH", "Library services belong to user scope.");
113
+ if (ctx.config.dataInstance.startsWith("user:"))
114
+ fail("INVALID_CONFIG", "The user library requires its local data instance.");
115
+ const scope = { kind: "user", id: ctx.scope.id };
116
+ const workspace = ctx.plugins.get(ctx.config.dataInstance);
117
+ const binding = (actorId) => workspace.bind({ scope, ownerPackage, packageVersion, actorId, access: ["data.workspace"] });
118
+ const data = binding("local");
119
+ data.register(materialDefinition);
120
+ data.register(usageDefinition);
121
+ const localCall = (call, operation) => {
122
+ const originalRequestKey = call.originalRequestKey ?? call.requestKey;
123
+ if (!call.requestKey || call.requestKey.length > 128 || !originalRequestKey || originalRequestKey.length > 128)
124
+ fail("INVALID_REQUEST_KEY", "Request keys must contain 1\u2013128 characters.");
125
+ const requestKey = "library-" + createHash("sha256").update(
126
+ JSON.stringify([
127
+ call.scope?.kind ?? scope.kind,
128
+ call.scope?.id ?? scope.id,
129
+ call.actor.id,
130
+ operation,
131
+ call.requestKey,
132
+ originalRequestKey,
133
+ call.sourceOperation ?? operation
134
+ ])
135
+ ).digest("hex");
136
+ return {
137
+ ...call,
138
+ scope,
139
+ requestKey,
140
+ originalRequestKey,
141
+ sourceOperation: call.sourceOperation ?? operation
142
+ };
143
+ };
144
+ const material = (api, objectId, includeDeleted = false) => {
145
+ const object = api.get(objectId, includeDeleted);
146
+ if (object.typeId !== "library.material") fail("TYPE_MISMATCH", "Expected a material.");
147
+ return object;
148
+ };
149
+ const assertProject = (projectId, call) => {
150
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(projectId) || call.scope?.kind !== "project" || call.scope.id !== projectId)
151
+ fail("SCOPE_MISMATCH", "Projects must actively report their own material usage.");
152
+ };
153
+ const handle = (name, input, handler, write = false, confirm = false) => ctx.handle(
154
+ defineOperation({
155
+ id: name,
156
+ title: name,
157
+ description: operationDescriptions[name] || "Operate on the specified scoped record using the declared schema and observed revision.",
158
+ input,
159
+ output: z2.any(),
160
+ effects: [write ? "write" : "read"],
161
+ confirm: confirm ? "user" : "none"
162
+ }),
163
+ handler
164
+ );
165
+ async function importFiles(input, call, snapshot2) {
166
+ const api = binding(call.actor.id);
167
+ const operationName = input.materialId ? "library.importVersion" : snapshot2 ? "library.collect" : "library.import";
168
+ const boundCall = localCall(call, operationName);
169
+ const receipt = {
170
+ scope: { ...call.scope ?? scope },
171
+ actorId: call.actor.id,
172
+ operation: operationName,
173
+ requestKey: boundCall.originalRequestKey,
174
+ invocationRequestKey: call.requestKey,
175
+ storageRequestKey: boundCall.requestKey,
176
+ sourceOperation: boundCall.sourceOperation
177
+ };
178
+ const replay = api.reserve(operationName, input, boundCall);
179
+ if (replay) return replay.result;
180
+ const identityCall = { ...call, requestKey: boundCall.requestKey };
181
+ const operationId = stable(api.storeId, identityCall, operationName, "import"), objectId = input.materialId || stable(operationId, identityCall, operationName, "object"), versionId = stable(operationId, identityCall, operationName, "version");
182
+ let stagingIds = input.stagingFileIds;
183
+ if (snapshot2) {
184
+ stagingIds = [];
185
+ for (const file of snapshot2.files) {
186
+ const staged = await api.stage(file.source, file.originalName, call.actor.id);
187
+ if (staged.sha256 !== file.sha256 || staged.sizeBytes !== file.sizeBytes)
188
+ fail("FILE_CORRUPT", "Snapshot bytes do not match the fixed source file.");
189
+ stagingIds.push(staged.stagingFileId);
190
+ }
191
+ }
192
+ const published = await api.publish(
193
+ stagingIds,
194
+ objectId,
195
+ versionId,
196
+ operationId,
197
+ call.actor.id,
198
+ snapshot2?.files.map((file) => file.role)
199
+ );
200
+ if (snapshot2 && published.kind !== snapshot2.kind)
201
+ fail("TYPE_MISMATCH", "Snapshot bytes do not match the stated kind.");
202
+ const result = api.operation(operationName, input, boundCall, (transaction) => {
203
+ const object = input.materialId ? transaction.get(objectId) : transaction.create(
204
+ "library.material",
205
+ materialData.parse({
206
+ name: input.name || snapshot2?.sourceLabel.objectName,
207
+ kind: published.kind,
208
+ origin: snapshot2 ? "collected" : "import",
209
+ sourceRef: snapshot2?.ref || null,
210
+ sourceLabel: snapshot2?.sourceLabel || null,
211
+ recommendedVersionId: versionId
212
+ }),
213
+ snapshot2?.metadata || {},
214
+ objectId
215
+ );
216
+ if (object.typeId !== "library.material" || object.data.kind !== published.kind)
217
+ fail("TYPE_MISMATCH", "Content kind does not match the material.");
218
+ const version = transaction.commitVersion(
219
+ objectId,
220
+ input.expectedRevision || object.revision,
221
+ {
222
+ versionId,
223
+ parentVersionId: input.parentVersionId,
224
+ sourceRef: snapshot2?.ref,
225
+ origin: snapshot2 ? "copied" : "upload",
226
+ payload: {
227
+ ...snapshot2?.payload || {},
228
+ ...published.details,
229
+ kind: published.kind,
230
+ primaryFileId: published.files[0].fileId,
231
+ attachments: published.files.slice(1).map((file) => ({ fileId: file.fileId, role: file.role })),
232
+ generation: snapshot2?.payload?.generation || null
233
+ },
234
+ metadata: snapshot2?.metadata,
235
+ files: published.files
236
+ }
237
+ );
238
+ return { material: transaction.get(objectId), version, receipt };
239
+ });
240
+ await api.finishFiles(operationId);
241
+ return result;
242
+ }
243
+ const publish = async (input, call) => {
244
+ const projectId = input.projectId || (call.scope?.kind === "project" ? call.scope.id : "");
245
+ assertProject(projectId, call);
246
+ ref.parse(input.ref);
247
+ const label = materialFields.shape.sourceLabel.parse(input.sourceLabel);
248
+ if (!label || !input.files.length || input.files.length > 20 || input.files[0].role !== "primary" || input.payload?.primaryFileId !== input.files[0].fileId)
249
+ fail("INVALID_SNAPSHOT", "A fixed snapshot requires its primary file and source label.");
250
+ const { files, ...snapshotFields } = input;
251
+ const intent = {
252
+ ...snapshotFields,
253
+ projectId,
254
+ files: files.map(({ source: _source, ...file }) => file)
255
+ };
256
+ return importFiles(intent, call, input);
257
+ };
258
+ const snapshot = async (source) => {
259
+ ref.parse(source);
260
+ const object = material(data, source.objectId), version = data.version(source), files = data.files(source);
261
+ const snapshots = [];
262
+ for (const file of files) {
263
+ const location = await data.location(source, file.fileId);
264
+ snapshots.push({
265
+ ...file,
266
+ source: (async function* () {
267
+ yield* createReadStream(location.absolutePath);
268
+ })()
269
+ });
270
+ }
271
+ return { material: object, version, files: snapshots };
272
+ };
273
+ const reportUsage = (input, call) => {
274
+ const usage = usageSchema.parse(input);
275
+ assertProject(usage.projectId, call);
276
+ const api = binding(call.actor.id);
277
+ material(api, usage.materialRef.objectId, true);
278
+ api.version(usage.materialRef);
279
+ const usageId = identity([
280
+ usage.materialRef,
281
+ usage.projectId,
282
+ usage.targetAssetId,
283
+ usage.targetVersionId
284
+ ]);
285
+ return api.operation(
286
+ "library.usage.upsert",
287
+ usage,
288
+ localCall(call, "library.usage.upsert"),
289
+ (transaction) => {
290
+ let current;
291
+ try {
292
+ current = transaction.get(usageId, true);
293
+ } catch (error) {
294
+ if (error.code !== "OBJECT_NOT_FOUND") throw error;
295
+ }
296
+ return current ? transaction.update(usageId, current.revision, usage) : transaction.create("library.usage", usage, {}, usageId);
297
+ }
298
+ );
299
+ };
300
+ const removeUsage = (input, call) => {
301
+ const removal = z2.object({
302
+ projectId: z2.string().min(1).max(128),
303
+ targetAssetId: id,
304
+ targetVersionId: id.optional(),
305
+ materialRef: ref.optional()
306
+ }).strict().parse(input);
307
+ assertProject(removal.projectId, call);
308
+ const api = binding(call.actor.id);
309
+ return api.operation(
310
+ "library.usage.remove",
311
+ removal,
312
+ localCall(call, "library.usage.remove"),
313
+ (transaction) => {
314
+ let offset = 0, removed = 0;
315
+ while (true) {
316
+ const page = api.list({
317
+ typeId: "library.usage",
318
+ targetAssetId: removal.targetAssetId,
319
+ limit: 200,
320
+ offset
321
+ });
322
+ for (const current of page.items) {
323
+ const usage = current.data;
324
+ if (usage.active && usage.projectId === removal.projectId && (!removal.targetVersionId || usage.targetVersionId === removal.targetVersionId) && (!removal.materialRef || ["storeId", "objectId", "versionId"].every(
325
+ (key) => usage.materialRef[key] === removal.materialRef[key]
326
+ ))) {
327
+ transaction.update(current.objectId, current.revision, { ...usage, active: false });
328
+ removed++;
329
+ }
330
+ }
331
+ if (page.nextOffset === null) return { removed };
332
+ offset = page.nextOffset;
333
+ }
334
+ }
335
+ );
336
+ };
337
+ const service = {
338
+ data,
339
+ publish,
340
+ snapshot,
341
+ reportUsage,
342
+ upsertUsage: reportUsage,
343
+ removeUsage
344
+ };
345
+ ctx.provide(service);
346
+ handle(
347
+ "library.list",
348
+ z2.object({
349
+ search: z2.string().optional(),
350
+ kind: z2.string().optional(),
351
+ tag: z2.string().optional(),
352
+ metadataEquals: z2.record(z2.string(), z2.any()).optional(),
353
+ metadataContains: z2.record(z2.string(), z2.any()).optional(),
354
+ sort: z2.enum(["name", "createdAt", "updatedAt"]).optional(),
355
+ direction: z2.enum(["asc", "desc"]).optional(),
356
+ limit: z2.number().int().min(1).max(200).optional(),
357
+ offset: z2.number().int().nonnegative().optional()
358
+ }).strict(),
359
+ async (input) => {
360
+ const result = data.list({ typeId: "library.material", ...input });
361
+ return {
362
+ ...result,
363
+ items: await Promise.all(
364
+ result.items.map(async (item) => {
365
+ const versions = data.versions(item.objectId);
366
+ return {
367
+ ...item,
368
+ versions,
369
+ usageCount: data.usageCount(item.objectId),
370
+ availability: versions.length ? await data.availability({
371
+ storeId: data.storeId,
372
+ objectId: item.objectId,
373
+ versionId: versions.at(-1).versionId
374
+ }) : "no_content"
375
+ };
376
+ })
377
+ )
378
+ };
379
+ }
380
+ );
381
+ handle("library.get", z2.object({ materialId: id }).strict(), (input) => ({
382
+ material: material(data, input.materialId, true),
383
+ versions: data.versions(input.materialId),
384
+ usages: data.usages({ objectId: input.materialId })
385
+ }));
386
+ handle(
387
+ "library.import",
388
+ z2.object({ name: z2.string().min(1).max(200), stagingFileIds: z2.array(id).min(1).max(20) }).strict(),
389
+ importFiles,
390
+ true
391
+ );
392
+ handle(
393
+ "library.importVersion",
394
+ z2.object({
395
+ materialId: id,
396
+ expectedRevision: revision,
397
+ parentVersionId: id.optional(),
398
+ stagingFileIds: z2.array(id).min(1).max(20)
399
+ }).strict(),
400
+ importFiles,
401
+ true
402
+ );
403
+ handle(
404
+ "library.update",
405
+ z2.object({
406
+ materialId: id,
407
+ expectedRevision: revision,
408
+ changes: materialFields.omit({ kind: true, origin: true, sourceRef: true, sourceLabel: true }).extend({
409
+ tags: materialFields.shape.tags.unwrap(),
410
+ category: materialFields.shape.category.unwrap(),
411
+ description: materialFields.shape.description.unwrap(),
412
+ lifecycle: materialFields.shape.lifecycle.unwrap(),
413
+ licenseNote: materialFields.shape.licenseNote.unwrap()
414
+ }).partial().strict()
415
+ }).strict(),
416
+ (input, call) => {
417
+ const api = binding(call.actor.id);
418
+ return api.operation(
419
+ "library.update",
420
+ input,
421
+ localCall(call, "library.update"),
422
+ (transaction) => {
423
+ const object = material(api, input.materialId);
424
+ if (input.changes.recommendedVersionId)
425
+ api.version({
426
+ storeId: api.storeId,
427
+ objectId: object.objectId,
428
+ versionId: input.changes.recommendedVersionId
429
+ });
430
+ return transaction.update(object.objectId, input.expectedRevision, {
431
+ ...object.data,
432
+ ...input.changes
433
+ });
434
+ }
435
+ );
436
+ },
437
+ true
438
+ );
439
+ for (const action of ["delete", "restore"])
440
+ handle(
441
+ "library." + action,
442
+ z2.object({ materialId: id, expectedRevision: revision }).strict(),
443
+ (input, call) => {
444
+ const api = binding(call.actor.id);
445
+ return api.operation(
446
+ "library." + action,
447
+ input,
448
+ localCall(call, "library." + action),
449
+ (transaction) => {
450
+ const object = material(api, input.materialId, true);
451
+ return transaction.update(
452
+ object.objectId,
453
+ input.expectedRevision,
454
+ object.data,
455
+ void 0,
456
+ action === "delete" ? (/* @__PURE__ */ new Date()).toISOString() : null
457
+ );
458
+ }
459
+ );
460
+ },
461
+ true
462
+ );
463
+ handle(
464
+ "library.usage",
465
+ z2.object({
466
+ materialId: id,
467
+ versionId: id.optional(),
468
+ limit: z2.number().int().min(1).max(200).optional(),
469
+ offset: z2.number().int().nonnegative().optional()
470
+ }).strict(),
471
+ (input) => {
472
+ material(data, input.materialId, true);
473
+ const limit = input.limit ?? 50, offset = input.offset ?? 0;
474
+ const items = data.usages({
475
+ objectId: input.materialId,
476
+ versionId: input.versionId,
477
+ limit,
478
+ offset
479
+ });
480
+ const hasMore = data.usages({
481
+ objectId: input.materialId,
482
+ versionId: input.versionId,
483
+ limit: 1,
484
+ offset: offset + limit
485
+ }).length > 0;
486
+ return { items, nextOffset: hasMore ? offset + limit : null };
487
+ }
488
+ );
489
+ const locationInput = z2.union([
490
+ z2.object({ source: ref, fileId: id.optional() }).strict(),
491
+ z2.object({ ref, fileId: id.optional() }).strict()
492
+ ]);
493
+ handle("files.resolveLocation", locationInput, (input) => {
494
+ material(data, (input.source || input.ref).objectId, true);
495
+ return data.location(input.source || input.ref, input.fileId);
496
+ });
497
+ handle("files.openLocation", locationInput, async (input) => {
498
+ material(data, (input.source || input.ref).objectId, true);
499
+ const location = await data.location(input.source || input.ref, input.fileId);
500
+ if (process.platform !== "darwin")
501
+ fail("OPEN_LOCATION_UNAVAILABLE", "Open-location is not verified on this platform.");
502
+ const { execFile } = await import("child_process"), { promisify } = await import("util");
503
+ await promisify(execFile)("open", ["-R", location.absolutePath]);
504
+ return location;
505
+ });
506
+ handle(
507
+ "metadata.patch",
508
+ z2.object({
509
+ objectId: id,
510
+ expectedRevision: revision,
511
+ set: z2.record(z2.string(), z2.any()),
512
+ remove: z2.array(z2.string())
513
+ }).strict(),
514
+ (input, call) => {
515
+ const api = binding(call.actor.id);
516
+ material(api, input.objectId);
517
+ return api.patchMetadata(
518
+ input.objectId,
519
+ input.expectedRevision,
520
+ input.set,
521
+ input.remove,
522
+ localCall(call, "metadata.patch")
523
+ );
524
+ },
525
+ true
526
+ );
527
+ handle(
528
+ "metadata.keys",
529
+ z2.object({ limit: z2.number().int().min(1).max(200).optional() }).strict(),
530
+ (input) => data.metadataKeys("library.material", input.limit)
531
+ );
532
+ handle("library.cleanupImpact", z2.object({ materialId: id }).strict(), (input) => {
533
+ material(data, input.materialId, true);
534
+ return data.impact(input.materialId);
535
+ });
536
+ handle(
537
+ "library.purge",
538
+ z2.object({ materialId: id, expectedRevision: revision }).strict(),
539
+ (input, call) => {
540
+ const api = binding(call.actor.id);
541
+ material(api, input.materialId, true);
542
+ return api.purge(input.materialId, input.expectedRevision, localCall(call, "objects.purge"));
543
+ },
544
+ true,
545
+ true
546
+ );
547
+ }
548
+ function identity(value) {
549
+ const hash = createHash("sha256").update(JSON.stringify(value)).digest("hex");
550
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
551
+ }
552
+ function stable(seed, call, operation, name) {
553
+ if (!call.requestKey) fail("INVALID_REQUEST_KEY", "A request key is required.");
554
+ return identity([
555
+ seed,
556
+ call.scope?.kind ?? "user",
557
+ call.scope?.id ?? "user",
558
+ call.actor.id,
559
+ operation,
560
+ call.requestKey,
561
+ name
562
+ ]);
563
+ }
564
+ export {
565
+ Config,
566
+ activate as default,
567
+ dependencies
568
+ };
@@ -0,0 +1,341 @@
1
+ import { CallContext as CallContext$1, z } from 'vivix-studio/sdk/shared';
2
+
3
+ type Scope = {
4
+ kind: 'user' | 'project';
5
+ id: string;
6
+ };
7
+ type CallContext = CallContext$1 & {
8
+ scope?: Scope;
9
+ confirmed?: boolean;
10
+ originalRequestKey?: string;
11
+ sourceOperation?: string;
12
+ };
13
+ type JSONValue = null | boolean | number | string | JSONValue[] | {
14
+ [key: string]: JSONValue;
15
+ };
16
+ interface RecordValue {
17
+ storeId: string;
18
+ scope: Scope;
19
+ objectId: string;
20
+ typeId: string;
21
+ schemaVersion: number;
22
+ revision: number;
23
+ data: any;
24
+ metadata: Record<string, JSONValue>;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ deletedAt: string | null;
28
+ }
29
+ interface VersionRef {
30
+ storeId: string;
31
+ objectId: string;
32
+ versionId: string;
33
+ }
34
+ interface VersionValue {
35
+ versionId: string;
36
+ objectId: string;
37
+ versionNumber: number;
38
+ schemaVersion: number;
39
+ parentVersionId: string | null;
40
+ sourceRef: VersionRef | null;
41
+ origin: string;
42
+ taskId: string | null;
43
+ payload: any;
44
+ metadata: any;
45
+ contentDigest: string;
46
+ createdAt: string;
47
+ }
48
+ interface FileValue {
49
+ fileId: string;
50
+ relativePath: string;
51
+ originalName: string;
52
+ mimeType: string;
53
+ sizeBytes: number;
54
+ sha256: string;
55
+ createdAt: string;
56
+ role: string;
57
+ ordinal: number;
58
+ }
59
+ type ProbeRunner = (path: string, signal?: AbortSignal, context?: {
60
+ directory: string;
61
+ }) => Promise<{
62
+ stdout: string;
63
+ stderr: string;
64
+ }>;
65
+ interface StageOptions {
66
+ signal?: AbortSignal;
67
+ probeRunner?: ProbeRunner;
68
+ onPrepared?: (stagingFileId: string) => void | Promise<void>;
69
+ }
70
+ interface StagedFile {
71
+ stagingFileId: string;
72
+ kind: string;
73
+ mimeType: string;
74
+ sizeBytes: number;
75
+ sha256: string;
76
+ originalName: string;
77
+ details: Record<string, any>;
78
+ }
79
+ type SqlScalar = null | string | number;
80
+ interface SqlInput {
81
+ sql: string;
82
+ params?: SqlScalar[] | Record<string, SqlScalar>;
83
+ timeoutMs?: number;
84
+ maxRows?: number;
85
+ limit?: number;
86
+ offset?: number;
87
+ }
88
+ type SqlRow = Record<string, JSONValue>;
89
+ interface MutationInput {
90
+ table: string;
91
+ action: 'insert' | 'update' | 'delete';
92
+ key?: Record<string, SqlScalar>;
93
+ values?: Record<string, SqlScalar>;
94
+ expectedRevision?: number;
95
+ }
96
+ interface MutationResult {
97
+ changes: number;
98
+ items: SqlRow[];
99
+ }
100
+ interface Query {
101
+ typeId: string;
102
+ search?: string;
103
+ kind?: string;
104
+ tag?: string;
105
+ targetAssetId?: string;
106
+ taskId?: string;
107
+ outputIndex?: number;
108
+ metadataEquals?: Record<string, JSONValue>;
109
+ metadataContains?: Record<string, JSONValue>;
110
+ sort?: 'name' | 'createdAt' | 'updatedAt';
111
+ direction?: 'asc' | 'desc';
112
+ limit?: number;
113
+ offset?: number;
114
+ includeDeleted?: boolean;
115
+ }
116
+ interface Transaction {
117
+ get(id: string, includeDeleted?: boolean): RecordValue;
118
+ create(typeId: string, data: any, metadata?: any, id?: string): RecordValue;
119
+ update(id: string, expectedRevision: number, data: any, metadata?: any, deletedAt?: string | null): RecordValue;
120
+ commitVersion(id: string, expectedRevision: number, value: {
121
+ parentVersionId?: string | null;
122
+ sourceRef?: VersionRef | null;
123
+ origin: string;
124
+ taskId?: string | null;
125
+ payload: any;
126
+ metadata?: any;
127
+ files?: FileValue[];
128
+ versionId?: string;
129
+ }): VersionValue;
130
+ /** Clear only mutable record-level references owned by this binding; version references remain immutable. */
131
+ removeRelations(sourceObjectId: string, kind: 'reference'): void;
132
+ relation(sourceObjectId: string, sourceVersionId: string | null, target: VersionRef, kind: 'source' | 'input' | 'reference'): void;
133
+ }
134
+ interface DataAPI {
135
+ readonly storeId: string;
136
+ readonly scope: Scope;
137
+ get(id: string, includeDeleted?: boolean): RecordValue;
138
+ list(query: Query): {
139
+ items: RecordValue[];
140
+ nextOffset: number | null;
141
+ };
142
+ query(input: SqlInput): Promise<SqlRow[]>;
143
+ mutate(input: MutationInput, call: CallContext): Promise<MutationResult>;
144
+ versions(id: string): VersionValue[];
145
+ version(ref: VersionRef): VersionValue;
146
+ files(ref: VersionRef): FileValue[];
147
+ receipt(name: string, input: unknown, call: CallContext): {
148
+ result: any;
149
+ } | null;
150
+ reserve(name: string, input: unknown, call: CallContext): {
151
+ result: any;
152
+ } | null;
153
+ operation<T>(name: string, input: unknown, call: CallContext, action: (tx: Transaction) => T): T;
154
+ patchMetadata(id: string, revision: number, set: Record<string, JSONValue>, remove: string[], call: CallContext): RecordValue;
155
+ metadataKeys(typeId: string, limit?: number): {
156
+ key: string;
157
+ types: string[];
158
+ observedCount: number;
159
+ }[];
160
+ stage(source: AsyncIterable<Uint8Array>, originalName: string, actorId: string, options?: StageOptions): Promise<StagedFile>;
161
+ stageText(text: string, actorId: string): Promise<StagedFile>;
162
+ publish(stagingIds: string[], objectId: string, versionId: string, operationId: string, actorId: string, fileRoles?: string[]): Promise<{
163
+ files: FileValue[];
164
+ details: any;
165
+ kind: string;
166
+ }>;
167
+ finishFiles(operationId: string): Promise<void>;
168
+ discardStaging(stagingFileId: string): Promise<void>;
169
+ discardPublication(operationId: string): Promise<void>;
170
+ location(ref: VersionRef, fileId?: string): Promise<{
171
+ source: VersionRef;
172
+ absolutePath: string;
173
+ fileId: string;
174
+ mimeType: string;
175
+ availability: 'ready';
176
+ previewUrl: string | null;
177
+ }>;
178
+ availability(ref: VersionRef): Promise<'ready' | 'missing' | 'corrupt'>;
179
+ openFile(ref: VersionRef, fileId: string, signal: AbortSignal): Promise<ReadableStream<Uint8Array>>;
180
+ usages(ref: {
181
+ objectId: string;
182
+ versionId?: string;
183
+ limit?: number;
184
+ offset?: number;
185
+ }): any[];
186
+ usageCount(objectId: string): number;
187
+ documents(action: 'create' | 'update' | 'delete', input: any, call: CallContext): RecordValue;
188
+ impact(id: string): any;
189
+ purge(id: string, revision: number, call: CallContext): any;
190
+ }
191
+
192
+ declare const Config: z.ZodDiscriminatedUnion<[z.ZodObject<{
193
+ role: z.ZodLiteral<"service">;
194
+ dataInstance: z.ZodDefault<z.ZodString>;
195
+ serviceInstance: z.ZodDefault<z.ZodString>;
196
+ }, z.core.$strict>, z.ZodObject<{
197
+ role: z.ZodLiteral<"client">;
198
+ serviceInstance: z.ZodDefault<z.ZodString>;
199
+ }, z.core.$strict>], "role">;
200
+ type MaterialConfig = z.infer<typeof Config>;
201
+ interface SnapshotFile extends FileValue {
202
+ source: AsyncIterable<Uint8Array>;
203
+ }
204
+ interface MaterialPublishInput {
205
+ projectId?: string;
206
+ ref: VersionRef;
207
+ sourceLabel: {
208
+ projectName: string;
209
+ objectName: string;
210
+ versionNumber: number;
211
+ };
212
+ name?: string;
213
+ kind: 'text' | 'image' | 'video' | 'audio' | 'file';
214
+ payload: any;
215
+ metadata?: Record<string, any>;
216
+ files: SnapshotFile[];
217
+ }
218
+ interface MaterialSnapshot {
219
+ material: RecordValue;
220
+ version: VersionValue;
221
+ files: SnapshotFile[];
222
+ }
223
+ interface UsageReport {
224
+ materialRef: VersionRef;
225
+ projectId: string;
226
+ targetAssetId: string;
227
+ targetVersionId: string;
228
+ name?: string;
229
+ active?: boolean;
230
+ }
231
+ interface UsageRemoval {
232
+ projectId: string;
233
+ targetAssetId: string;
234
+ materialRef?: VersionRef;
235
+ targetVersionId?: string;
236
+ }
237
+ interface LibraryReceipt {
238
+ scope: Scope;
239
+ actorId: string;
240
+ operation: string;
241
+ requestKey: string;
242
+ invocationRequestKey: string;
243
+ storageRequestKey: string;
244
+ sourceOperation: string;
245
+ }
246
+ interface LibraryService {
247
+ readonly data: DataAPI;
248
+ publish(input: MaterialPublishInput, call: CallContext): Promise<{
249
+ material: RecordValue;
250
+ version: VersionValue;
251
+ receipt?: LibraryReceipt;
252
+ }>;
253
+ snapshot(ref: VersionRef): Promise<MaterialSnapshot>;
254
+ reportUsage(input: UsageReport, call: CallContext): RecordValue;
255
+ upsertUsage(input: UsageReport, call: CallContext): RecordValue;
256
+ removeUsage(input: UsageRemoval, call: CallContext): {
257
+ removed: number;
258
+ };
259
+ }
260
+ declare const materialFields: z.ZodObject<{
261
+ name: z.ZodString;
262
+ kind: z.ZodEnum<{
263
+ file: "file";
264
+ text: "text";
265
+ image: "image";
266
+ video: "video";
267
+ audio: "audio";
268
+ }>;
269
+ tags: z.ZodDefault<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string[], string[]>>>;
270
+ category: z.ZodDefault<z.ZodEnum<{
271
+ character: "character";
272
+ scene: "scene";
273
+ style: "style";
274
+ sound: "sound";
275
+ reference: "reference";
276
+ other: "other";
277
+ }>>;
278
+ description: z.ZodDefault<z.ZodString>;
279
+ origin: z.ZodEnum<{
280
+ import: "import";
281
+ collected: "collected";
282
+ }>;
283
+ sourceRef: z.ZodDefault<z.ZodNullable<z.ZodObject<{
284
+ storeId: z.ZodUUID;
285
+ objectId: z.ZodUUID;
286
+ versionId: z.ZodUUID;
287
+ }, z.core.$strict>>>;
288
+ sourceLabel: z.ZodDefault<z.ZodNullable<z.ZodObject<{
289
+ projectName: z.ZodString;
290
+ objectName: z.ZodString;
291
+ versionNumber: z.ZodNumber;
292
+ }, z.core.$strict>>>;
293
+ lifecycle: z.ZodDefault<z.ZodEnum<{
294
+ ready: "ready";
295
+ archived: "archived";
296
+ }>>;
297
+ recommendedVersionId: z.ZodUUID;
298
+ licenseNote: z.ZodDefault<z.ZodString>;
299
+ }, z.core.$strict>;
300
+ declare const materialData: z.ZodObject<{
301
+ name: z.ZodString;
302
+ kind: z.ZodEnum<{
303
+ file: "file";
304
+ text: "text";
305
+ image: "image";
306
+ video: "video";
307
+ audio: "audio";
308
+ }>;
309
+ tags: z.ZodDefault<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string[], string[]>>>;
310
+ category: z.ZodDefault<z.ZodEnum<{
311
+ character: "character";
312
+ scene: "scene";
313
+ style: "style";
314
+ sound: "sound";
315
+ reference: "reference";
316
+ other: "other";
317
+ }>>;
318
+ description: z.ZodDefault<z.ZodString>;
319
+ origin: z.ZodEnum<{
320
+ import: "import";
321
+ collected: "collected";
322
+ }>;
323
+ sourceRef: z.ZodDefault<z.ZodNullable<z.ZodObject<{
324
+ storeId: z.ZodUUID;
325
+ objectId: z.ZodUUID;
326
+ versionId: z.ZodUUID;
327
+ }, z.core.$strict>>>;
328
+ sourceLabel: z.ZodDefault<z.ZodNullable<z.ZodObject<{
329
+ projectName: z.ZodString;
330
+ objectName: z.ZodString;
331
+ versionNumber: z.ZodNumber;
332
+ }, z.core.$strict>>>;
333
+ lifecycle: z.ZodDefault<z.ZodEnum<{
334
+ ready: "ready";
335
+ archived: "archived";
336
+ }>>;
337
+ recommendedVersionId: z.ZodUUID;
338
+ licenseNote: z.ZodDefault<z.ZodString>;
339
+ }, z.core.$strict>;
340
+
341
+ export { Config, type LibraryReceipt, type LibraryService, type MaterialConfig, type MaterialPublishInput, type MaterialSnapshot, type SnapshotFile, type UsageRemoval, type UsageReport, materialData, materialFields };
package/dist/shared.js ADDED
@@ -0,0 +1,39 @@
1
+ // packages/material-library/src/shared.ts
2
+ import { z } from "vivix-studio/sdk/shared";
3
+ var Config = z.discriminatedUnion("role", [
4
+ z.object({
5
+ role: z.literal("service"),
6
+ dataInstance: z.string().min(1).default("data"),
7
+ serviceInstance: z.string().min(1).default("library")
8
+ }).strict(),
9
+ z.object({
10
+ role: z.literal("client"),
11
+ serviceInstance: z.string().min(1).default("user:library")
12
+ }).strict()
13
+ ]);
14
+ var materialFields = z.object({
15
+ name: z.string().trim().min(1).max(200),
16
+ kind: z.enum(["text", "image", "video", "audio", "file"]),
17
+ tags: z.array(z.string().trim().min(1).max(64)).max(50).transform((x) => [...new Set(x)]).default([]),
18
+ category: z.enum(["character", "scene", "style", "sound", "reference", "other"]).default("other"),
19
+ description: z.string().max(4e3).default(""),
20
+ origin: z.enum(["import", "collected"]),
21
+ sourceRef: z.object({ storeId: z.uuid(), objectId: z.uuid(), versionId: z.uuid() }).strict().nullable().default(null),
22
+ sourceLabel: z.object({
23
+ projectName: z.string(),
24
+ objectName: z.string(),
25
+ versionNumber: z.number().int().positive()
26
+ }).strict().nullable().default(null),
27
+ lifecycle: z.enum(["ready", "archived"]).default("ready"),
28
+ recommendedVersionId: z.uuid(),
29
+ licenseNote: z.string().max(4e3).default("")
30
+ }).strict();
31
+ var materialData = materialFields.refine(
32
+ (v) => v.origin !== "collected" || v.sourceRef !== null,
33
+ "Collected materials require a fixed source."
34
+ );
35
+ export {
36
+ Config,
37
+ materialData,
38
+ materialFields
39
+ };
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@vivix-labs/material-library",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-beta",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/",
8
+ "tag": "beta"
9
+ },
10
+ "license": "MIT",
11
+ "files": [
12
+ "dist",
13
+ "studio-plugin.json",
14
+ "skills",
15
+ "presets"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.12.0"
19
+ },
20
+ "exports": {
21
+ "./studio-plugin.json": "./studio-plugin.json",
22
+ "./server": {
23
+ "types": "./dist/server.d.ts",
24
+ "import": "./dist/server.js"
25
+ },
26
+ "./shared": {
27
+ "types": "./dist/shared.d.ts",
28
+ "import": "./dist/shared.js"
29
+ },
30
+ "./client": {
31
+ "types": "./dist/client.d.ts",
32
+ "import": "./dist/client.js"
33
+ }
34
+ },
35
+ "peerDependencies": {
36
+ "vivix-studio": ">=2.1.0-0",
37
+ "@vivix-labs/creative-data": "1.0.0-beta",
38
+ "react": "19.3.1"
39
+ },
40
+ "devDependencies": {
41
+ "@vivix-labs/creative-data": "1.0.0-beta"
42
+ },
43
+ "scripts": {
44
+ "build": "node ../../scripts/build-product-packages.mjs material-library"
45
+ }
6
46
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "manifestVersion": "0.2",
3
+ "id": "material-library",
4
+ "requirements": { "studioPluginApi": "^0.2.0" },
5
+ "requestedAccess": []
6
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.