@sequenceholdings/artifact-studio 0.2.5 → 0.2.6

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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Artifact Studio SDK
2
+
3
+ `@sequenceholdings/artifact-studio` exposes the host-provided `seq` client. Runtime
4
+ requests use the current user's normal permissions.
5
+
6
+ Artifact Studio is a workflow-agnostic platform primitive. The host, SDK, and
7
+ deployer provide generic application capabilities; each deployed artifact owns
8
+ its product behavior. Primitive code must not branch on an artifact ID or import
9
+ an application's services. See [Platform primitive boundaries](../../../docs/platform/platform-primitives.md).
10
+
11
+ ## Grouping calls into a trace
12
+
13
+ The Artifact host supplies tracing context automatically for supported platform
14
+ requests. Use an explicit action scope to correlate several calls from one UI
15
+ action and supply a readable intent key:
16
+
17
+ ```ts
18
+ import { withTraceAction } from '@sequenceholdings/artifact-studio'
19
+
20
+ await withTraceAction(
21
+ { key: 'application.refresh' },
22
+ async ({ api, functions }) => {
23
+ await functions.invoke('my-function', { refresh: true })
24
+ await api.get('/api/my-application/summary')
25
+ },
26
+ )
27
+ ```
28
+
29
+ Use a stable operation label, never an identifier, customer value or payload. Each
30
+ scope gets its own correlation ID, so concurrent actions remain separate. The
31
+ helper scopes JSON `api.fetch/get/post/put/patch/delete` and `functions.invoke`;
32
+ streaming and file-transfer helpers retain their ordinary host tracing behavior.
33
+ Tracing does not grant permissions, change service-account behavior or guarantee
34
+ that every transport is instrumented. Traces availability depends on the deployed
35
+ platform version and your app/resource permissions.
package/VERSION-PIN.md CHANGED
@@ -53,8 +53,12 @@ never claims `no-op` for a deploy that would actually rebuild.
53
53
 
54
54
  `tokens.css` is the canonical stylesheet and includes semantic roles plus the
55
55
  Tailwind utility bridge. The legacy `tokens.v2.css` and `theme.v2.css` paths are
56
- temporary forwarding aliases; the compiler normalizes their source imports to one
57
- canonical layer and reports a migration warning. An explicit
56
+ temporary forwarding aliases in current packages; the compiler normalizes their
57
+ source imports to one canonical layer and reports a migration warning only after
58
+ checking that both selected files are forwarding aliases. Older packages carry
59
+ independent styles in those files, so their imports and provenance entries are
60
+ preserved. This prevents CRM spacing and corner utilities from disappearing when
61
+ rebuilding with a split-sheet package. An explicit
58
62
  `seq-studio artifact dev --link <atlas-ui-package-dir>` preview additionally
59
63
  reports retired numbered black/white opacity utilities and uncertain dynamic
60
64
  class construction. Legacy stylesheet aliases normalize to canonical tokens;
@@ -1,11 +1,15 @@
1
1
  import type { Plugin } from 'vite';
2
2
  export interface AtlasUiResolvedTokenSelection {
3
- specifier: '@sequenceholdings/atlas-ui/tokens.css';
3
+ specifier: string;
4
4
  injectedBy: string | null;
5
5
  }
6
+ /** Only collapse aliases when the selected package makes them forwarding sheets.
7
+ * Older releases define independent semantic tokens and the Tailwind bridge there.
8
+ */
9
+ export declare function hasUnifiedAtlasUiTokens(atlasUiDir: string): boolean;
6
10
  /**
7
- * Every supported source shape becomes one canonical import before Tailwind
8
- * expands it. The aliases remain package-compatible, but are no longer allowed
11
+ * For packages with forwarding aliases, collapse imports before Tailwind
12
+ * expands them. The aliases remain package-compatible, but are no longer allowed
9
13
  * to create duplicate token layers in an Artifact Studio bundle (DES-437).
10
14
  */
11
15
  export declare function normalizeAtlasUiTokenImports(code: string): {
@@ -13,10 +17,10 @@ export declare function normalizeAtlasUiTokenImports(code: string): {
13
17
  deprecatedImports: string[];
14
18
  };
15
19
  /**
16
- * Evidence mirrors the CSS transform: aliases are normalized away, so the only
17
- * selected sheet is the canonical token entry point.
20
+ * Evidence mirrors the selected package: split sheets remain separate entries;
21
+ * forwarding aliases are normalized to the canonical entry point.
18
22
  */
19
- export declare function collectAtlasUiResolvedTokenSelections({ cssModules, }: {
23
+ export declare function collectAtlasUiResolvedTokenSelections({ cssModules, atlasUiDir, }: {
20
24
  cssModules: readonly {
21
25
  id: string;
22
26
  code: string;
@@ -25,7 +29,7 @@ export declare function collectAtlasUiResolvedTokenSelections({ cssModules, }: {
25
29
  atlasUiDist: string;
26
30
  formRendererDist: string | null;
27
31
  }): AtlasUiResolvedTokenSelection[];
28
- export declare function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist, formRendererDist, onTransform, }: {
32
+ export declare function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, onTransform, }: {
29
33
  atlasUiDir: string;
30
34
  atlasUiDist: string;
31
35
  formRendererDist: string | null;
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -19,14 +19,26 @@ function resolveTwAnimateCssPath() {
19
19
  }
20
20
  const CANONICAL_TOKEN_IMPORT = '@import "@sequenceholdings/atlas-ui/tokens.css";';
21
21
  const ATLAS_UI_TOKEN_IMPORT = /@import\s+(?:url\(\s*)?(?:["'][^"']*?@sequenceholdings\/atlas-ui\/|[^"'()\s;]*@sequenceholdings\/atlas-ui\/)(tokens\.css|tokens\.v2\.css|theme\.v2\.css)(?:["'])?\s*\)?[^;\n]*;/gi;
22
+ /** Only collapse aliases when the selected package makes them forwarding sheets.
23
+ * Older releases define independent semantic tokens and the Tailwind bridge there.
24
+ */
25
+ export function hasUnifiedAtlasUiTokens(atlasUiDir) {
26
+ return ['tokens.v2.css', 'theme.v2.css'].every((sheet) => {
27
+ const path = resolve(atlasUiDir, sheet);
28
+ if (!existsSync(path))
29
+ return false;
30
+ const content = readFileSync(path, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').trim();
31
+ return /^@import\s+["']@sequenceholdings\/atlas-ui\/tokens\.css["'];$/.test(content);
32
+ });
33
+ }
22
34
  function atlasUiTokenImportSheets(code) {
23
35
  return [...code.matchAll(ATLAS_UI_TOKEN_IMPORT)]
24
36
  .map((match) => match[1]?.toLowerCase())
25
37
  .filter((sheet) => sheet !== undefined);
26
38
  }
27
39
  /**
28
- * Every supported source shape becomes one canonical import before Tailwind
29
- * expands it. The aliases remain package-compatible, but are no longer allowed
40
+ * For packages with forwarding aliases, collapse imports before Tailwind
41
+ * expands them. The aliases remain package-compatible, but are no longer allowed
30
42
  * to create duplicate token layers in an Artifact Studio bundle (DES-437).
31
43
  */
32
44
  export function normalizeAtlasUiTokenImports(code) {
@@ -43,16 +55,23 @@ export function normalizeAtlasUiTokenImports(code) {
43
55
  };
44
56
  }
45
57
  /**
46
- * Evidence mirrors the CSS transform: aliases are normalized away, so the only
47
- * selected sheet is the canonical token entry point.
58
+ * Evidence mirrors the selected package: split sheets remain separate entries;
59
+ * forwarding aliases are normalized to the canonical entry point.
48
60
  */
49
- export function collectAtlasUiResolvedTokenSelections({ cssModules, }) {
61
+ export function collectAtlasUiResolvedTokenSelections({ cssModules, atlasUiDir, }) {
50
62
  for (const { code } of cssModules) {
51
63
  if (!code.includes('@import "tailwindcss"'))
52
64
  continue;
65
+ const sheets = atlasUiTokenImportSheets(code);
66
+ if (!hasUnifiedAtlasUiTokens(atlasUiDir)) {
67
+ return [...new Set(['tokens.css', ...sheets])].map((sheet) => ({
68
+ specifier: `@sequenceholdings/atlas-ui/${sheet}`,
69
+ injectedBy: sheets.includes(sheet) ? null : 'atlas-ui-css-resolver',
70
+ }));
71
+ }
53
72
  return [{
54
73
  specifier: '@sequenceholdings/atlas-ui/tokens.css',
55
- injectedBy: atlasUiTokenImportSheets(code).includes('tokens.css') ? null : 'atlas-ui-css-resolver',
74
+ injectedBy: sheets.includes('tokens.css') ? null : 'atlas-ui-css-resolver',
56
75
  }];
57
76
  }
58
77
  return [];
@@ -70,7 +89,8 @@ function insertAfterImports(code, lines) {
70
89
  const end = lastImport.index + lastImport[0].length;
71
90
  return `${code.slice(0, end)}${block}${code.slice(end)}`;
72
91
  }
73
- export function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist, formRendererDist, onTransform, }) {
92
+ export function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, onTransform, }) {
93
+ const unifiedTokens = hasUnifiedAtlasUiTokens(atlasUiDir);
74
94
  return {
75
95
  name: 'atlas-ui-css-resolver',
76
96
  enforce: 'pre',
@@ -88,8 +108,16 @@ export function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist,
88
108
  if (formRendererDist) {
89
109
  sources.push(`@source ${JSON.stringify(formRendererDist)};`);
90
110
  }
91
- const tokenImports = normalizeAtlasUiTokenImports(result);
92
- result = tokenImports.code;
111
+ if (unifiedTokens) {
112
+ result = normalizeAtlasUiTokenImports(result).code;
113
+ }
114
+ else if (!atlasUiTokenImportSheets(result).includes('tokens.css')) {
115
+ const firstTokenImport = result.match(ATLAS_UI_TOKEN_IMPORT)?.[0];
116
+ // The old base must precede the semantic overrides in split packages.
117
+ result = firstTokenImport
118
+ ? result.replace(firstTokenImport, `${CANONICAL_TOKEN_IMPORT}\n${firstTokenImport}`)
119
+ : insertAfterImports(result, [CANONICAL_TOKEN_IMPORT]);
120
+ }
93
121
  result = insertAfterImports(result, sources);
94
122
  }
95
123
  if (result.includes('@import "tw-animate-css"')) {
package/dist/build.js CHANGED
@@ -3,7 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, relative, resolve } from 'node:path';
5
5
  import { build as viteBuild } from 'vite';
6
- import { atlasUiCssResolverPlugin, collectAtlasUiResolvedTokenSelections } from './atlas-ui-css-plugin.js';
6
+ import { atlasUiCssResolverPlugin, collectAtlasUiResolvedTokenSelections, hasUnifiedAtlasUiTokens } from './atlas-ui-css-plugin.js';
7
7
  import { validateAtlasUiTokenStylesheetContract } from './atlas-ui-package-contract.js';
8
8
  import { createObservedBuildEvidence } from './build-provenance.js';
9
9
  import { collectResolvedStylesheets } from './resolved-stylesheets.js';
@@ -31,6 +31,7 @@ const reactRouterDomDir = dirname(cliRequire.resolve('react-router-dom/package.j
31
31
  const sonnerDir = dirname(cliRequire.resolve('sonner'));
32
32
  const atlasUiDir = dirname(cliRequire.resolve('@sequenceholdings/atlas-ui/package.json'));
33
33
  const atlasUiDist = resolve(atlasUiDir, 'dist');
34
+ const artifactSdkEntry = cliRequire.resolve('@sequenceholdings/artifact-studio');
34
35
  /**
35
36
  * Released Atlas UI 2.0.1 exports its canonical sheet from dist, while the
36
37
  * candidate layout puts the generated entry at package root so it can be read
@@ -75,6 +76,7 @@ function createPeerResolveAlias({ atlasUiPackageDir = atlasUiDir } = {}) {
75
76
  '@tanstack/react-query': reactQueryDir,
76
77
  'react-router-dom': reactRouterDomDir,
77
78
  sonner: sonnerDir,
79
+ '@sequenceholdings/storage': cliRequire.resolve('@sequenceholdings/storage'),
78
80
  // Candidate Atlas UI exposes canonical tokens at the package root; released
79
81
  // 2.0.1 keeps them in dist. Preserve explicit alias resolution so legacy
80
82
  // source reaches the normalizer instead of falling through to the bare alias.
@@ -102,7 +104,9 @@ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompat
102
104
  const selectedAtlasUiTokens = resolveAtlasUiTokensPath(selectedAtlasUiDir);
103
105
  const source = await readArtifactStudioSource(rootDir);
104
106
  const diagnostics = [
105
- ...collectDeprecatedTokenImportDiagnostics({ files: source.files }),
107
+ ...(hasUnifiedAtlasUiTokens(selectedAtlasUiDir)
108
+ ? collectDeprecatedTokenImportDiagnostics({ files: source.files })
109
+ : []),
106
110
  ...(candidateTokenCompatibility
107
111
  ? collectCandidateTokenCompatibilityDiagnostics({ files: source.files })
108
112
  : []),
@@ -116,7 +120,7 @@ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompat
116
120
  plugins: [
117
121
  rejectOutsideSourcePlugin({
118
122
  root,
119
- trustedExternalRoots: Object.values(peerResolveAlias).map((target) => dirname(target)),
123
+ trustedExternalRoots: [...Object.values(peerResolveAlias).map((target) => dirname(target)), dirname(artifactSdkEntry)],
120
124
  }),
121
125
  rejectIgnoredSourcePlugin(root, source.ignorePatterns),
122
126
  artifactSdkVirtualModule(),
@@ -267,8 +271,8 @@ function artifactSdkVirtualModule() {
267
271
  // host-injected `window.seq`, so a tree that happens to be `pnpm install`ed
268
272
  // must produce the same bundle as a bare tree (e.g. one materialized from a
269
273
  // git repo) — otherwise the same source yields different bytes depending on
270
- // whether node_modules exists. The package's sole runtime export is `seq`
271
- // (everything else is type-only), so this stub is a complete substitute.
274
+ // whether node_modules exists. Use the CLI's SDK entry so runtime helpers
275
+ // stay in sync with the types instead of maintaining a partial export stub.
272
276
  enforce: 'pre',
273
277
  resolveId(id) {
274
278
  return virtualIds.has(id) ? resolved : undefined;
@@ -276,7 +280,7 @@ function artifactSdkVirtualModule() {
276
280
  load(id) {
277
281
  if (id !== resolved)
278
282
  return undefined;
279
- return 'export const seq = window.seq; export default window.seq;';
283
+ return `export * from ${JSON.stringify(artifactSdkEntry)}; export { default } from ${JSON.stringify(artifactSdkEntry)};`;
280
284
  },
281
285
  };
282
286
  }
package/dist/cli.d.ts CHANGED
@@ -5,7 +5,6 @@ interface RemoteProject {
5
5
  id: string;
6
6
  slug: string;
7
7
  title: string;
8
- visibility?: string;
9
8
  activeDeployment?: {
10
9
  version: string;
11
10
  sourceHash: string;
package/dist/cli.js CHANGED
@@ -254,8 +254,7 @@ async function listCommand(args) {
254
254
  : project.activeDeploymentId
255
255
  ? 'deployed'
256
256
  : 'not deployed';
257
- const visibility = project.visibility ? ` ${project.visibility}` : '';
258
- console.log(` ${project.slug} id=${project.id} [${active}]${visibility} ${project.title}`);
257
+ console.log(` ${project.slug} id=${project.id} [${active}] ${project.title}`);
259
258
  }
260
259
  return 0;
261
260
  }
@@ -300,8 +299,6 @@ async function showCommand(args) {
300
299
  console.log(` title: ${project.title}`);
301
300
  if (project.description)
302
301
  console.log(` description: ${project.description}`);
303
- if (project.visibility)
304
- console.log(` visibility: ${project.visibility}`);
305
302
  if (project.appEligible !== undefined)
306
303
  console.log(` appEligible: ${project.appEligible ? 'yes' : 'no'}`);
307
304
  if (project.createdBy)
@@ -499,7 +496,6 @@ async function deployCommand(args) {
499
496
  linkConfigDir: spec.kind === 'local' ? source.dir : null,
500
497
  allowCreate: args.flags['no-create'] !== true,
501
498
  explicitProjectId,
502
- envName: env.name,
503
499
  });
504
500
  const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', provenance: source.provenance });
505
501
  console.log(`[seq-studio] deployed ${project.title} ${deployment.version}`);
@@ -589,9 +585,7 @@ async function devCommand(args) {
589
585
  // Resolve per push so the token refreshes across a long watch session.
590
586
  const token = await getRequiredToken(args);
591
587
  if (!projectId) {
592
- // envName so a first-time `artifact dev` creation honors the manifest
593
- // visibility too (and notes drift, once, when the project pre-exists).
594
- const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir, envName: env.name });
588
+ const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir });
595
589
  projectId = project.id;
596
590
  }
597
591
  const deployment = await uploadDeployment({
@@ -771,21 +765,7 @@ export function activeDeploymentMatchesSkipPin({ active, sourceHash, peerVersion
771
765
  return (active.cliVersion === peerVersions.cliVersion &&
772
766
  active.atlasUiVersion === peerVersions.atlasUiVersion);
773
767
  }
774
- /**
775
- * Deploys never change a live project's visibility — the Studio sharing UI /
776
- * PATCH own that. The manifest's targets.<env>.visibility is honored at
777
- * project CREATION only; on drift, say so instead of silently ignoring the
778
- * field (which is how it read as decorative for a year).
779
- */
780
- function warnOnVisibilityDrift({ project, declared, envName }) {
781
- if (!declared || !envName || !project.visibility || declared === project.visibility)
782
- return;
783
- console.warn(`[seq-studio] note: artifact.bundle.yml targets.${envName}.visibility is "${declared}" but the project is ` +
784
- `"${project.visibility}" on this environment — deploys never change visibility. Update it in Studio sharing ` +
785
- `(or PATCH /api/artifact-studio/projects/${project.id}) if the manifest is what you intend.`);
786
- }
787
- async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null, envName = null }) {
788
- const declaredVisibility = envName ? result.manifest.targets?.[envName]?.visibility : undefined;
768
+ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null }) {
789
769
  if (explicitProjectId) {
790
770
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${explicitProjectId}` });
791
771
  if (!data)
@@ -794,14 +774,12 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
794
774
  throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
795
775
  `artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
796
776
  }
797
- warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
798
777
  return data.project;
799
778
  }
800
779
  const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
801
780
  if (config.projectId) {
802
781
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
803
782
  if (data) {
804
- warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
805
783
  return data.project;
806
784
  }
807
785
  console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
@@ -810,7 +788,6 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
810
788
  if (existing) {
811
789
  if (linkConfigDir)
812
790
  await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
813
- warnOnVisibilityDrift({ project: existing, declared: declaredVisibility, envName });
814
791
  return existing;
815
792
  }
816
793
  if (!allowCreate) {
@@ -826,7 +803,6 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
826
803
  title: result.manifest.artifact.title,
827
804
  slug: result.manifest.artifact.project_id,
828
805
  description: result.manifest.artifact.description ?? null,
829
- ...(declaredVisibility ? { visibility: declaredVisibility } : {}),
830
806
  },
831
807
  });
832
808
  if (linkConfigDir)
@@ -29,6 +29,11 @@ export declare const artifactStudioManifestSchema: z.ZodObject<{
29
29
  react: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<18>, z.ZodLiteral<19>]>>;
30
30
  }, z.core.$strip>>;
31
31
  capabilities: z.ZodDefault<z.ZodObject<{
32
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
33
+ viewer: "viewer";
34
+ editor: "editor";
35
+ admin: "admin";
36
+ }>>>;
32
37
  data: z.ZodDefault<z.ZodObject<{
33
38
  read: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
39
  write: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -57,12 +62,7 @@ export declare const artifactStudioManifestSchema: z.ZodObject<{
57
62
  }, z.core.$strip>>;
58
63
  targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
59
64
  url: z.ZodString;
60
- visibility: z.ZodDefault<z.ZodEnum<{
61
- published: "published";
62
- private: "private";
63
- shared: "shared";
64
- }>>;
65
- }, z.core.$strip>>>;
65
+ }, z.core.$strict>>>;
66
66
  sidebarBadge: z.ZodOptional<z.ZodObject<{
67
67
  endpoint: z.ZodString;
68
68
  tone: z.ZodOptional<z.ZodEnum<{
package/dist/manifest.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { storageScopesSchema } from '@sequenceholdings/storage';
1
2
  import { z } from 'zod';
2
3
  import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins, } from './manifest-custom-roles.js';
3
4
  import { normalizeArtifactPath } from './paths.js';
@@ -56,6 +57,7 @@ export const artifactStudioManifestSchema = z.object({
56
57
  react: z.union([z.literal(18), z.literal(19)]).default(18),
57
58
  }).default({ sdk: 'sequence', react: 18 }),
58
59
  capabilities: z.object({
60
+ storage: storageScopesSchema.optional(),
59
61
  data: z.object({
60
62
  read: z.array(z.string().min(1)).default([]),
61
63
  write: z.array(z.string().min(1)).default([]),
@@ -96,8 +98,7 @@ export const artifactStudioManifestSchema = z.object({
96
98
  }),
97
99
  targets: z.record(z.string().min(1), z.object({
98
100
  url: z.string().url(),
99
- visibility: z.enum(['private', 'shared', 'published']).default('private'),
100
- })).default({}),
101
+ }).strict()).default({}),
101
102
  sidebarBadge: sidebarBadgeSchema.optional(),
102
103
  }).superRefine((manifest, ctx) => {
103
104
  for (const message of validateCapabilityPins({
package/dist/sdk.d.ts CHANGED
@@ -1,4 +1,22 @@
1
+ import { type StartStorageUpload, type StorageUploadSession } from '@sequenceholdings/storage';
2
+ export interface SequenceStorageOptions {
3
+ signal?: AbortSignal;
4
+ timeoutMs?: number;
5
+ onProgress?: (progress: {
6
+ uploadId: string;
7
+ fileId: string;
8
+ uploadedBytes: number;
9
+ }) => void;
10
+ }
11
+ export interface SequenceStorage {
12
+ upload(workspaceId: string, input: StartStorageUpload, file: Blob, options?: SequenceStorageOptions): Promise<StorageUploadSession>;
13
+ resume(workspaceId: string, uploadId: string, file: Blob, options?: SequenceStorageOptions): Promise<StorageUploadSession>;
14
+ }
1
15
  export interface SequenceApiFetchOptions {
16
+ traceAction?: {
17
+ id: string;
18
+ key: string;
19
+ };
2
20
  method?: string;
3
21
  body?: unknown;
4
22
  headers?: Record<string, string>;
@@ -7,6 +25,8 @@ export interface SequenceApiFetchResponse<TBody = unknown> {
7
25
  ok: boolean;
8
26
  status: number;
9
27
  body: TBody;
28
+ /** Allowlisted download headers for binary responses; keys are lowercase. */
29
+ headers?: Record<string, string>;
10
30
  }
11
31
  export interface SequenceApiUploadOptions {
12
32
  timeoutMs?: number;
@@ -28,7 +48,10 @@ export interface SequenceApi {
28
48
  delete<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
29
49
  }
30
50
  export interface SequenceFunctions {
31
- invoke<TOutput = unknown>(id: string, params?: Record<string, unknown>): Promise<TOutput>;
51
+ invoke<TOutput = unknown>(id: string, params?: Record<string, unknown>, traceAction?: {
52
+ id: string;
53
+ key: string;
54
+ }): Promise<TOutput>;
32
55
  }
33
56
  export interface SequenceUser {
34
57
  current(): Promise<{
@@ -109,6 +132,7 @@ export interface SequenceHost {
109
132
  }): void;
110
133
  }
111
134
  export interface SequenceArtifactSdk {
135
+ storage: SequenceStorage;
112
136
  api: SequenceApi;
113
137
  ai: {
114
138
  fetch(path: string, options?: SequenceApiFetchOptions): Promise<Response>;
@@ -128,3 +152,398 @@ declare global {
128
152
  }
129
153
  export declare const seq: SequenceArtifactSdk;
130
154
  export default seq;
155
+ /** Typed metadata API plus direct browser-host transfers for sandboxed Artifacts. */
156
+ export declare function createArtifactStorageClient(sdk?: SequenceArtifactSdk): {
157
+ workspace(id: string): {
158
+ uploads: {
159
+ put: (input: StartStorageUpload, file: Blob, options?: SequenceStorageOptions) => Promise<{
160
+ id: string;
161
+ fileId: string;
162
+ uploadUrl: string | null;
163
+ expiresAt: string;
164
+ state: "aborted" | "pending" | "complete";
165
+ versionId: string | null;
166
+ maxBytes: number;
167
+ }>;
168
+ resume: (uploadId: string, file: Blob, options?: SequenceStorageOptions) => Promise<{
169
+ id: string;
170
+ fileId: string;
171
+ uploadUrl: string | null;
172
+ expiresAt: string;
173
+ state: "aborted" | "pending" | "complete";
174
+ versionId: string | null;
175
+ maxBytes: number;
176
+ }>;
177
+ start: (input: StartStorageUpload) => Promise<{
178
+ id: string;
179
+ fileId: string;
180
+ uploadUrl: string | null;
181
+ expiresAt: string;
182
+ state: "pending" | "complete" | "aborted";
183
+ versionId: string | null;
184
+ maxBytes: number;
185
+ }>;
186
+ get: (id: string) => Promise<{
187
+ id: string;
188
+ fileId: string;
189
+ uploadUrl: string | null;
190
+ expiresAt: string;
191
+ state: "pending" | "complete" | "aborted";
192
+ versionId: string | null;
193
+ maxBytes: number;
194
+ }>;
195
+ complete: (id: string, input?: import("@sequenceholdings/storage").CompleteStorageUpload) => Promise<{
196
+ id: string;
197
+ fileId: string;
198
+ uploadUrl: string | null;
199
+ expiresAt: string;
200
+ state: "pending" | "complete" | "aborted";
201
+ versionId: string | null;
202
+ maxBytes: number;
203
+ }>;
204
+ abort: (id: string) => Promise<{
205
+ id: string;
206
+ fileId: string;
207
+ uploadUrl: string | null;
208
+ expiresAt: string;
209
+ state: "pending" | "complete" | "aborted";
210
+ versionId: string | null;
211
+ maxBytes: number;
212
+ }>;
213
+ };
214
+ get: () => Promise<{
215
+ id: string;
216
+ name: string;
217
+ description: string;
218
+ environment: string;
219
+ state: "active" | "trashed" | "purging";
220
+ revision: number;
221
+ createdAt: string;
222
+ role: "viewer" | "editor" | "admin" | "owner";
223
+ quotaBytes: number;
224
+ usedBytes: number;
225
+ reservedBytes: number;
226
+ }>;
227
+ delete: (expectedRevision: number) => Promise<{
228
+ id: string;
229
+ workspaceId: string;
230
+ targetId: string | null;
231
+ kind: "purge" | "delete_version";
232
+ status: "queued" | "running" | "succeeded" | "failed";
233
+ completed: number;
234
+ total: number;
235
+ error: string | null;
236
+ createdAt: string;
237
+ updatedAt: string;
238
+ canRetry: boolean;
239
+ }>;
240
+ list: (options?: import("@sequenceholdings/storage").StorageListOptions & {
241
+ parentId?: string | null;
242
+ trash?: boolean;
243
+ }) => Promise<{
244
+ items: {
245
+ id: string;
246
+ workspaceId: string;
247
+ parentId: string | null;
248
+ name: string;
249
+ kind: "file" | "folder";
250
+ state: "active" | "trashed" | "purging";
251
+ restricted: boolean;
252
+ revision: number;
253
+ currentVersionId: string | null;
254
+ metadata: Record<string, string>;
255
+ createdAt: string;
256
+ updatedAt: string;
257
+ role: "viewer" | "editor" | "admin";
258
+ size: number | null;
259
+ contentType: string | null;
260
+ }[];
261
+ nextCursor: string | null;
262
+ }>;
263
+ getNode: (id: string) => Promise<{
264
+ id: string;
265
+ workspaceId: string;
266
+ parentId: string | null;
267
+ name: string;
268
+ kind: "file" | "folder";
269
+ state: "active" | "trashed" | "purging";
270
+ restricted: boolean;
271
+ revision: number;
272
+ currentVersionId: string | null;
273
+ metadata: Record<string, string>;
274
+ createdAt: string;
275
+ updatedAt: string;
276
+ role: "viewer" | "editor" | "admin";
277
+ size: number | null;
278
+ contentType: string | null;
279
+ }>;
280
+ createFolder: (name: string, parentId?: string | null, { idempotencyKey, }?: {
281
+ idempotencyKey?: string;
282
+ }) => Promise<{
283
+ id: string;
284
+ workspaceId: string;
285
+ parentId: string | null;
286
+ name: string;
287
+ kind: "file" | "folder";
288
+ state: "active" | "trashed" | "purging";
289
+ restricted: boolean;
290
+ revision: number;
291
+ currentVersionId: string | null;
292
+ metadata: Record<string, string>;
293
+ createdAt: string;
294
+ updatedAt: string;
295
+ role: "viewer" | "editor" | "admin";
296
+ size: number | null;
297
+ contentType: string | null;
298
+ }>;
299
+ update: (id: string, input: import("zod").input<typeof import("@sequenceholdings/storage").updateNodeSchema>) => Promise<{
300
+ id: string;
301
+ workspaceId: string;
302
+ parentId: string | null;
303
+ name: string;
304
+ kind: "file" | "folder";
305
+ state: "active" | "trashed" | "purging";
306
+ restricted: boolean;
307
+ revision: number;
308
+ currentVersionId: string | null;
309
+ metadata: Record<string, string>;
310
+ createdAt: string;
311
+ updatedAt: string;
312
+ role: "viewer" | "editor" | "admin";
313
+ size: number | null;
314
+ contentType: string | null;
315
+ }>;
316
+ trash: (id: string, expectedRevision: number) => Promise<{
317
+ id: string;
318
+ workspaceId: string;
319
+ parentId: string | null;
320
+ name: string;
321
+ kind: "file" | "folder";
322
+ state: "active" | "trashed" | "purging";
323
+ restricted: boolean;
324
+ revision: number;
325
+ currentVersionId: string | null;
326
+ metadata: Record<string, string>;
327
+ createdAt: string;
328
+ updatedAt: string;
329
+ role: "viewer" | "editor" | "admin";
330
+ size: number | null;
331
+ contentType: string | null;
332
+ }>;
333
+ restore: (id: string, expectedRevision: number) => Promise<{
334
+ id: string;
335
+ workspaceId: string;
336
+ parentId: string | null;
337
+ name: string;
338
+ kind: "file" | "folder";
339
+ state: "active" | "trashed" | "purging";
340
+ restricted: boolean;
341
+ revision: number;
342
+ currentVersionId: string | null;
343
+ metadata: Record<string, string>;
344
+ createdAt: string;
345
+ updatedAt: string;
346
+ role: "viewer" | "editor" | "admin";
347
+ size: number | null;
348
+ contentType: string | null;
349
+ }>;
350
+ purge: (id: string, expectedRevision: number) => Promise<{
351
+ id: string;
352
+ workspaceId: string;
353
+ targetId: string | null;
354
+ kind: "purge" | "delete_version";
355
+ status: "queued" | "running" | "succeeded" | "failed";
356
+ completed: number;
357
+ total: number;
358
+ error: string | null;
359
+ createdAt: string;
360
+ updatedAt: string;
361
+ canRetry: boolean;
362
+ }>;
363
+ download: (id: string, options?: {
364
+ versionId?: string;
365
+ preview?: boolean;
366
+ }) => Promise<{
367
+ url: string;
368
+ expiresAt: string;
369
+ preview: boolean;
370
+ }>;
371
+ versions: (id: string, options?: {
372
+ before?: number;
373
+ limit?: number;
374
+ }) => Promise<{
375
+ items: {
376
+ id: string;
377
+ fileId: string;
378
+ revision: number;
379
+ size: number;
380
+ checksum: string;
381
+ contentType: string;
382
+ createdAt: string;
383
+ createdBy: string;
384
+ state: "active" | "purging";
385
+ }[];
386
+ nextRevision: number | null;
387
+ }>;
388
+ restoreVersion: (id: string, versionId: string, expectedRevision: number) => Promise<{
389
+ id: string;
390
+ fileId: string;
391
+ revision: number;
392
+ size: number;
393
+ checksum: string;
394
+ contentType: string;
395
+ createdAt: string;
396
+ createdBy: string;
397
+ state: "active" | "purging";
398
+ }>;
399
+ deleteVersion: (id: string, versionId: string, expectedRevision: number) => Promise<{
400
+ id: string;
401
+ workspaceId: string;
402
+ targetId: string | null;
403
+ kind: "purge" | "delete_version";
404
+ status: "queued" | "running" | "succeeded" | "failed";
405
+ completed: number;
406
+ total: number;
407
+ error: string | null;
408
+ createdAt: string;
409
+ updatedAt: string;
410
+ canRetry: boolean;
411
+ }>;
412
+ access: (id?: string) => Promise<{
413
+ revision: number;
414
+ restricted: boolean;
415
+ grants: {
416
+ type: "user" | "group" | "organization" | "service_account";
417
+ id: string;
418
+ role: "viewer" | "editor" | "admin";
419
+ }[];
420
+ owner: string | null;
421
+ inherited: boolean;
422
+ workspaceAdminOverride: true;
423
+ displayNames: Record<string, string>;
424
+ }>;
425
+ setAccess: (input: import("zod").input<typeof import("@sequenceholdings/storage").updateStorageAccessSchema>, id?: string) => Promise<{
426
+ revision: number;
427
+ restricted: boolean;
428
+ grants: {
429
+ type: "user" | "group" | "organization" | "service_account";
430
+ id: string;
431
+ role: "viewer" | "editor" | "admin";
432
+ }[];
433
+ owner: string | null;
434
+ inherited: boolean;
435
+ workspaceAdminOverride: true;
436
+ displayNames: Record<string, string>;
437
+ }>;
438
+ transferOwner: (principal: {
439
+ type: "user" | "service_account";
440
+ id: string;
441
+ }, expectedRevision: number) => Promise<{
442
+ revision: number;
443
+ restricted: boolean;
444
+ grants: {
445
+ type: "user" | "group" | "organization" | "service_account";
446
+ id: string;
447
+ role: "viewer" | "editor" | "admin";
448
+ }[];
449
+ owner: string | null;
450
+ inherited: boolean;
451
+ workspaceAdminOverride: true;
452
+ displayNames: Record<string, string>;
453
+ }>;
454
+ operations: (options?: {
455
+ before?: string;
456
+ limit?: number;
457
+ scope?: import("@sequenceholdings/storage").StorageOperationScope;
458
+ }) => Promise<{
459
+ items: {
460
+ id: string;
461
+ workspaceId: string;
462
+ targetId: string | null;
463
+ kind: "purge" | "delete_version";
464
+ status: "queued" | "running" | "succeeded" | "failed";
465
+ completed: number;
466
+ total: number;
467
+ error: string | null;
468
+ createdAt: string;
469
+ updatedAt: string;
470
+ canRetry: boolean;
471
+ }[];
472
+ nextCursor: string | null;
473
+ }>;
474
+ retryOperation: (id: string) => Promise<{
475
+ id: string;
476
+ workspaceId: string;
477
+ targetId: string | null;
478
+ kind: "purge" | "delete_version";
479
+ status: "queued" | "running" | "succeeded" | "failed";
480
+ completed: number;
481
+ total: number;
482
+ error: string | null;
483
+ createdAt: string;
484
+ updatedAt: string;
485
+ canRetry: boolean;
486
+ }>;
487
+ };
488
+ capabilities: () => Promise<{
489
+ canProvision: boolean;
490
+ uploadMethods: string[];
491
+ }>;
492
+ list: (options?: import("@sequenceholdings/storage").StorageListOptions) => Promise<{
493
+ items: {
494
+ id: string;
495
+ name: string;
496
+ description: string;
497
+ environment: string;
498
+ state: "active" | "trashed" | "purging";
499
+ revision: number;
500
+ createdAt: string;
501
+ role: "viewer" | "editor" | "admin" | "owner";
502
+ quotaBytes: number;
503
+ usedBytes: number;
504
+ reservedBytes: number;
505
+ }[];
506
+ nextCursor: string | null;
507
+ }>;
508
+ shared: (options?: import("@sequenceholdings/storage").StorageListOptions & {
509
+ trash?: boolean;
510
+ }) => Promise<{
511
+ items: {
512
+ id: string;
513
+ workspaceId: string;
514
+ parentId: string | null;
515
+ name: string;
516
+ kind: "file" | "folder";
517
+ state: "active" | "trashed" | "purging";
518
+ restricted: boolean;
519
+ revision: number;
520
+ currentVersionId: string | null;
521
+ metadata: Record<string, string>;
522
+ createdAt: string;
523
+ updatedAt: string;
524
+ role: "viewer" | "editor" | "admin";
525
+ size: number | null;
526
+ contentType: string | null;
527
+ }[];
528
+ nextCursor: string | null;
529
+ }>;
530
+ apply: (manifest: import("@sequenceholdings/storage").WorkspaceManifest) => Promise<{
531
+ id: string;
532
+ name: string;
533
+ description: string;
534
+ environment: string;
535
+ state: "active" | "trashed" | "purging";
536
+ revision: number;
537
+ createdAt: string;
538
+ role: "viewer" | "editor" | "admin" | "owner";
539
+ quotaBytes: number;
540
+ usedBytes: number;
541
+ reservedBytes: number;
542
+ }>;
543
+ };
544
+ /** Explicit scope keeps concurrent browser actions isolated. The key is a safe
545
+ * author label, never a payload or an authentication claim. */
546
+ export declare function withTraceAction<T>({ key, sdk }: {
547
+ key: string;
548
+ sdk?: SequenceArtifactSdk;
549
+ }, run: (client: Pick<SequenceArtifactSdk, 'api' | 'functions'>) => Promise<T>): Promise<T>;
package/dist/sdk.js CHANGED
@@ -1,4 +1,39 @@
1
+ import { storageClient } from '@sequenceholdings/storage';
1
2
  export const seq = typeof window !== 'undefined'
2
3
  ? window.seq
3
4
  : undefined;
4
5
  export default seq;
6
+ /** Typed metadata API plus direct browser-host transfers for sandboxed Artifacts. */
7
+ export function createArtifactStorageClient(sdk = seq) {
8
+ const client = storageClient({ async request(method, path, body) {
9
+ const response = await sdk.api.fetch(path, { method, body });
10
+ if (!response.ok)
11
+ throw new Error(`Storage request failed (${response.status})`);
12
+ return response.body;
13
+ } });
14
+ return { ...client, workspace(id) {
15
+ const workspace = client.workspace(id);
16
+ return { ...workspace, uploads: { ...workspace.uploads,
17
+ put: (input, file, options) => sdk.storage.upload(id, input, file, options),
18
+ resume: (uploadId, file, options) => sdk.storage.resume(id, uploadId, file, options),
19
+ } };
20
+ } };
21
+ }
22
+ /** Explicit scope keeps concurrent browser actions isolated. The key is a safe
23
+ * author label, never a payload or an authentication claim. */
24
+ export function withTraceAction({ key, sdk = seq }, run) {
25
+ if (!/^[a-zA-Z0-9_.:/-]{1,120}$/.test(key))
26
+ throw new Error('Use a bounded operation key for tracing');
27
+ const traceAction = { id: crypto.randomUUID(), key };
28
+ const fetch = (path, options) => sdk.api.fetch(path, { ...options, traceAction });
29
+ const request = async (method, path, body) => {
30
+ const response = await fetch(path, { method, body });
31
+ if (!response.ok)
32
+ throw new Error(`API request failed (${response.status})`);
33
+ return response.body;
34
+ };
35
+ return run({ api: { ...sdk.api, fetch,
36
+ get: (path) => request('GET', path), post: (path, body) => request('POST', path, body),
37
+ put: (path, body) => request('PUT', path, body), patch: (path, body) => request('PATCH', path, body), delete: (path, body) => request('DELETE', path, body),
38
+ }, functions: { invoke: (id, params) => sdk.functions.invoke(id, params, traceAction) } });
39
+ }
@@ -29,4 +29,3 @@ capabilities:
29
29
  targets:
30
30
  local:
31
31
  url: http://localhost:5001
32
- visibility: private
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "0.2.5",
12
+ "version": "0.2.6",
13
13
  "description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
14
14
  "type": "module",
15
15
  "exports": {
@@ -85,7 +85,8 @@
85
85
  "tw-animate-css": "^1.4.0",
86
86
  "vite": "8.0.16",
87
87
  "zod": "^4.1.13",
88
- "@sequenceholdings/lattice-form-renderer": "0.1.3"
88
+ "@sequenceholdings/lattice-form-renderer": "0.1.3",
89
+ "@sequenceholdings/storage": "0.1.0"
89
90
  },
90
91
  "devDependencies": {
91
92
  "@types/js-yaml": "^4.0.9",
@@ -29,4 +29,3 @@ capabilities:
29
29
  targets:
30
30
  local:
31
31
  url: http://localhost:5001
32
- visibility: private