@sequenceholdings/artifact-studio 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +219 -0
  2. package/VERSION-PIN.md +15 -0
  3. package/dist/api.d.ts +5 -0
  4. package/dist/api.js +7 -0
  5. package/dist/artifact-env-bindings.d.ts +1 -1
  6. package/dist/artifact-env-bindings.js +5 -4
  7. package/dist/build-input-guards.d.ts +8 -0
  8. package/dist/build-input-guards.js +178 -0
  9. package/dist/build.js +14 -5
  10. package/dist/cli-content-readers.d.ts +8 -0
  11. package/dist/cli-content-readers.js +28 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +38 -6
  14. package/dist/{env-binding-scan.d.ts → env-binding-manifest.d.ts} +1 -4
  15. package/dist/env-binding-manifest.js +56 -0
  16. package/dist/env-bindings.js +2 -34
  17. package/dist/git-service-client.d.ts +11 -0
  18. package/dist/git-service-client.js +10 -0
  19. package/dist/manifest-next.d.ts +6 -0
  20. package/dist/manifest-next.js +3 -0
  21. package/dist/manifest.d.ts +4 -0
  22. package/dist/manifest.js +1 -0
  23. package/dist/next/bridge.d.ts +15 -1
  24. package/dist/next/bridge.js +18 -1
  25. package/dist/next-source-includes.d.ts +50 -0
  26. package/dist/next-source-includes.js +68 -0
  27. package/dist/prepare-build.d.ts +10 -2
  28. package/dist/prepare-build.js +33 -3
  29. package/dist/repository-identity.d.ts +31 -0
  30. package/dist/repository-identity.js +41 -0
  31. package/dist/runtime-contracts.d.ts +1 -4
  32. package/dist/runtime-contracts.js +2 -15
  33. package/dist/server/api-client.d.ts +1 -0
  34. package/dist/server/api-client.js +13 -7
  35. package/dist/server/group-directory.d.ts +90 -0
  36. package/dist/server/group-directory.js +17 -0
  37. package/dist/server/groups.d.ts +2 -1
  38. package/dist/server/groups.js +2 -0
  39. package/dist/server/human-completion.d.ts +44 -0
  40. package/dist/server/human-completion.js +38 -0
  41. package/dist/server/processes.d.ts +19 -0
  42. package/dist/server/processes.js +11 -0
  43. package/dist/server/task-assignments.d.ts +81 -0
  44. package/dist/server/task-assignments.js +26 -0
  45. package/dist/server.d.ts +4 -0
  46. package/dist/source-boundary.d.ts +27 -6
  47. package/dist/source-boundary.js +44 -30
  48. package/dist/source-exclusions.d.ts +4 -0
  49. package/dist/source-exclusions.js +16 -0
  50. package/dist/source-resolver.js +2 -1
  51. package/dist/templates/react-vite/package.json +1 -1
  52. package/package.json +6 -2
  53. package/templates/react-vite/package.json +1 -1
  54. package/dist/env-binding-scan.js +0 -179
package/README.md CHANGED
@@ -475,6 +475,13 @@ const body = await client.api.stream('/api/agents/chat', {
475
475
  return new Response(body, { headers: { 'content-type': 'text/event-stream' } })
476
476
  ```
477
477
 
478
+ `client.api.streamResponse(path, options)` uses the same streaming transport and
479
+ returns the full `Response`, retaining the broker-approved content type, length,
480
+ and download filename. It does not buffer the body. Forward `request.signal` to
481
+ cancel upstream work when the user disconnects. Governed content readers impose
482
+ their own stricter byte and whole-stream deadlines; this method does not extend
483
+ them. The method is prepared for 0.10.0 and awaits publication.
484
+
478
485
  ## Grouping calls into a trace
479
486
 
480
487
  The Artifact host supplies tracing context automatically for supported platform
@@ -514,3 +521,215 @@ Upload with `api.upload` option `{ fieldName: 'audio' }`; ordinary uploads still
514
521
  default to `file`. Atlas transcription trims silence by default; multipart
515
522
  `trimSilence=false` opts out. Speech uses the declared host paths without requiring
516
523
  the separate general Next host API rollout flag.
524
+
525
+ ## Next.js task completion and app navigation access (awaiting publication)
526
+
527
+ These methods require the matching Atlas host deployment. The SDK conveniences
528
+ are prepared for Artifact Studio 0.10.0; source availability does not mean the
529
+ package has been published.
530
+
531
+ ```ts
532
+ const appAccess = await bridge.auth.getAppAccess()
533
+ // Show links only when the destination's value is true. This is a UI hint;
534
+ // the destination still authorizes the user independently.
535
+
536
+ // After your app-owned route successfully advances its native process step:
537
+ const { notified } = await bridge.lattice.notifyAdvance()
538
+ // false means the server has not yet completed this human step. Retry this
539
+ // read after the accepted transition settles; never replay the mutation.
540
+ ```
541
+
542
+ `auth.getAppAccess` reads only the current viewer's boolean navigation map from
543
+ one fixed, authenticated host endpoint. It accepts no principal or URL,
544
+ requires no arbitrary `capabilities.api` declaration, and does not depend on
545
+ `artifact-next-browser-api`. Missing or malformed permission responses fail
546
+ closed. The generic bridge equivalent is
547
+ `bridge.request({ method: 'auth.getAppAccess' })`.
548
+
549
+ `lattice.notifyAdvance` accepts no run or step arguments. The host uses its own
550
+ trusted embed context and checks the native authorized run snapshot. Only the
551
+ exact completed human step produces an advance notification; pending, failed,
552
+ replaced or unauthorized tasks cannot. Repeated notifications are deduplicated
553
+ for that host instance. It never advances a process or changes assignments.
554
+ An app can call it before submitting as a host-support check: a parked step
555
+ returns `{ notified: false }`; an old host rejects the method. The generic
556
+ bridge equivalent is `bridge.request({ method: 'lattice.notifyAdvance' })`.
557
+ Applications outside a task embed have no completion context and cannot use it.
558
+
559
+
560
+ ### Bounded lifecycle reads and group management discovery
561
+
562
+ A server application can read exact run lifecycles in batches of at most 500
563
+ through `api.post('/api/lattice/runs/read', { runIds })`. Declare that POST path
564
+ under `server.capabilities.api.write` and each process under `processes.read`.
565
+ The adapter has no write effects. It requires the signed-in user, a declared
566
+ process (including development bindings), and Viewer on every run before
567
+ returning any result. Missing or denied runs fail the entire batch. Results
568
+ contain only `id`, `processId`, `status`, `startedAt`, `completedAt`, and
569
+ `archivedAt`; no process metadata, step context, or credentials are exposed.
570
+
571
+ The authorized group directory and detail responses include `canManage`,
572
+ derived from existing group administration or platform administration, and
573
+ `externalId`. Applications cannot confer group authority through their own
574
+ roles. `POST /api/groups/directory` accepts `manageableOnly: true`; it uses all
575
+ groups only for a verified platform administrator, and otherwise filters the
576
+ caller's existing accessible pages. In that mode the response is
577
+ `{ groups, nextPage: number | null }`, with no filtered-count claim. Follow
578
+ `nextPage` even when a page is empty. This read grants no memberships and changes no group.
579
+
580
+ ### Recoverable group task assignment
581
+
582
+ Human callers can use these declared `server.capabilities.api.write` adapters:
583
+
584
+ - `POST /api/lattice/group-task-assignment/prepare`: `{groupId, runId, stepId,
585
+ targetSub}` returns `{runId, stepId, nodeId, processId, processVersion,
586
+ expectedAssignmentToken, intentId, preparationProof}`. The server mints the
587
+ intent UUID. The signed proof binds the exact caller, project, group, command,
588
+ pinned process/step and actual SQL environment. The application must declare `processes.act` for
589
+ the mapped process and `groups.read`. The caller must independently administer
590
+ the live group and hold run Viewer. The task must be parked and unsubmitted,
591
+ currently assigned to that group or a member, and the target must be an active
592
+ group member permitted by the run's pinned node allowlist. Application roles
593
+ do not grant group administration.
594
+ - `POST /api/lattice/group-task-assignment/commit`: the same fields plus
595
+ `{intentId, expectedAssignmentToken, preparationProof}` return `{receipt}`. The UUID intent is
596
+ unique within the project. Native assignment, its authorization outbox, and
597
+ the immutable `applied` receipt commit in one transaction. Reusing an intent
598
+ with different actor, run, step, target, or token returns 409; an exact retry
599
+ never reapplies an old assignment.
600
+ - `POST /api/lattice/group-task-assignment/cancel`: the commit body returns the
601
+ stable receipt. Cancellation and commit serialize on the same intent key.
602
+ A `cancelled` receipt prevents any later assignment; an already `applied`
603
+ receipt is returned unchanged. Current group authority and run Viewer remain
604
+ required, but a disabled target or completed task does not prevent cancellation.
605
+ - `POST /api/lattice/group-task-assignment/receipt`: `{runId, intentId}` returns
606
+ `{receipt: object | null}` under the project's exact identity, declared
607
+ `processes.read`, and current run Viewer. It never returns another project's
608
+ receipt or exposes task state, context, or wait credentials.
609
+
610
+ A receipt contains project/intent/run/step/node identities, actor, principals,
611
+ `outcome: applied | cancelled`, and `committedAt`. Its timestamp is the database
612
+ transaction timestamp; the receipt becomes visible only after commit. A null
613
+ receipt is **not** proof that a command cannot still apply. App-owned ownership
614
+ reservations must finalize on `applied` and release only on `cancelled`. They
615
+ must not expire an ambiguous reservation or issue another intent as a retry.
616
+ The preparation proof has no expiry and grants no permission by itself. Signing
617
+ key rotation requires explicit operator recovery for pending commands. If the
618
+ caller loses group authority, recovery needs an explicitly authorized
619
+ operator; these endpoints never impersonate the original caller. The first
620
+ release provides caller-driven recovery, not a background service reconciler.
621
+
622
+ Deployment requires the host adapters and the journaled assignment receipt
623
+ migration. The app's ownership transaction and notification outbox remain
624
+ application-owned and require their own reviewed service grants.
625
+
626
+ `client.processes.getHumanCompletion({ runId, stepId })` reads a committed,
627
+ server-verified human completion receipt. Declare the exact
628
+ `/api/lattice/human-completion/read` path under `server.capabilities.api.write`
629
+ (the POST carries a body but has no write effects), plus the process under
630
+ `processes.read`. Only a verified human caller with current Run Viewer access can
631
+ read it. The result binds actor, immutable process version/bundle, exact step
632
+ attempt, edge and canonical payload hash; it never returns the payload, signing
633
+ token or wait credentials. The payload hash covers canonical JSON of the actual
634
+ submitted payload, including the server-stamped `advanced_by`, excluding the
635
+ reserved `_sequence_human_completion` receipt key. HTTP409
636
+ `HUMAN_COMPLETION_UNAVAILABLE` means no verified committed evidence is available;
637
+ it is not permission to replay a mutation. This method does not establish a
638
+ successor step or grant authority to assign one. The same adapter is callable
639
+ through `client.api.post` by earlier SDK clients, once the target platform release
640
+ is active. Policy-backed application writes still independently require the
641
+ reviewed ORM runtime capability and admission checks.
642
+
643
+ `client.processes.getHumanContinuation({ runId, stepId })` uses the corresponding
644
+ `/api/lattice/human-continuation/read` POST adapter with the same caller and process
645
+ read permissions. It returns `waiting`, `terminal`, or the first still-parked human
646
+ step and its materialized user/group assignments. The platform verifies the signed
647
+ source completion and every actual edge in a bounded contiguous native serial walk
648
+ against the pinned bundle. Ambiguous or truncated history, parallel/subprocess
649
+ paths, resets, intervening human completions and repeated nodes are refused with
650
+ HTTP409 `HUMAN_CONTINUATION_UNAVAILABLE`; timestamps or graph reachability alone
651
+ never establish a successor. This read grants no authority to advance or reassign.
652
+
653
+ `bridge.lattice.prepareAdvance()` verifies the host-owned embedded task and its caller Viewer admission without emitting a completion signal, including when the native step already completed. Apps with a separate durable handoff must first verify that handoff, then call `notifyAdvance()`. Both methods accept no task identifiers; the host supplies its own signed context.
654
+
655
+ ### Coordinating a human submission with application ownership (awaiting publication)
656
+
657
+ Official Next.js servers can coordinate a native human completion with a separate
658
+ application transaction through `/api/lattice/human-handoff/*`. These are fixed
659
+ `client.api.post` adapters: declare each exact path under `server.capabilities.api.write`
660
+ and the process under `processes.act` and `processes.read`. They require a verified
661
+ runtime credential, the gateway's authenticated human, current Run Viewer, and the
662
+ same application and original actor on every retry. Browser delegation, service
663
+ mode, a copied request context, and caller-supplied identity are refused. The
664
+ application server is trusted to acknowledge only its own committed transaction;
665
+ applications must not proxy acknowledgement directly from browser input.
666
+
667
+ The protocol is deliberately ordered:
668
+
669
+ 1. `prepare` takes `{runId, stepId, edgeId, payload}` and performs read-only support,
670
+ current Advancer and immutable graph checks. It returns an immutable command
671
+ with `id`, source pins, `payloadHash`, `assignmentToken` and `preparationProof`.
672
+ Store this complete command with the application intent and original payload.
673
+ 2. Reserve the application intent with its own CAS, then call `reserve` with that
674
+ exact prepared command. Native reservation checks the source assignment,
675
+ submission and mutation admissions under the run lock. A failure does not
676
+ authorize clearing the application reservation.
677
+ 3. `advance` takes `{id, runId, stepId, payload}`. Only the original immutable
678
+ command may signal the workflow. Admission is durable before dispatch; an HTTP
679
+ failure, Temporal signal ACK, or absent completion receipt is not cancellation.
680
+ 4. Use the verified completion and continuation readers. `reconcile` takes
681
+ `{id, runId, stepId, users, groups}` and binds the proved immediate successor.
682
+ Identical native assignments need no Owner grant. A real change retains native
683
+ Run Owner, destination grantability and allowlist requirements, with readback.
684
+ Terminal continuations require empty user/group arrays.
685
+ 5. Commit the application's atomic ownership/watchers/outbox transaction. Only
686
+ then call `acknowledge` with `{id, runId, stepId}`. `released` is required before
687
+ telling the browser the whole action succeeded. If acknowledgement is lost,
688
+ retry it; it never replays the application transaction.
689
+
690
+ `read` takes `{runId, stepId, id?}` and returns only fence identity/status and the
691
+ bound successor. Include `id` to recover an exact cancelled attempt. Without it,
692
+ only a non-cancelled source attempt is returned. It exposes no payload or token.
693
+
694
+ When the source assignment changes between preparation and reservation, `cancel`
695
+ accepts the exact prepared command and verifies its server signature, actor,
696
+ application and actual SQL environment. Under the same run lock it writes a
697
+ permanent cancellation receipt only if no matching native command was admitted
698
+ or unexplained settlement observed. A different immutable native admission proves
699
+ this command did not settle, even if that other command already completed.
700
+ Protocol-1 workers always require matching admission before consuming a signal.
701
+ A worker-only timeout receipt, written atomically with the timeout result and
702
+ pinned to the source attempt, also proves an unaccepted prepared command did
703
+ not complete when the timeout won before reservation. Payload fields and imported
704
+ runner output are never timeout evidence. The application may clear its own exact pending CAS only
705
+ after observing `status: 'cancelled'`. The cancelled ID can never reserve or
706
+ advance; a fresh prepared ID can retry the still-parked source. There is no timeout
707
+ unlock. A late request using a cancelled permit is refused before signal delivery.
708
+
709
+ **Activation boundaries:** this initial protocol admits only top-level Temporal
710
+ human waiters that advertise the patched native handoff protocol. Trigger wait
711
+ credentials and older in-flight waiters are unsupported. Existing human timeout durations remain in force. A held fence pauses settlement
712
+ of both its source timeout and any successor timeout until reconciliation or
713
+ exact cancellation; it does not remove the configured timeout. Every possible selected continuation must reach a first human or
714
+ terminal through at most 128 serial automation, managed-function, managed-server
715
+ or delay nodes. Parallel/subprocess/agent routes, loops, duplicate or missing
716
+ edges are rejected before reserving application state. Deploy migrations first,
717
+ then roll out the admission-enforcing API to every instance, then deploy patched
718
+ workers, and only then enable application use. Deploying workers before all API
719
+ callers admit signals can discard unadmitted old-version signals; an API rollout
720
+ alone is insufficient. Existing unsupported waiters need an independently
721
+ reviewed migration or must finish on their prior application path.
722
+
723
+ While held, native advance, assignment, sharing, cancellation, reset and fork
724
+ mutations cannot cross the fence. The worker discards stale unadmitted signals
725
+ and holds successor timeouts until release. Ordinary native commands are admitted
726
+ before dispatch, and a prior admitted command prevents fence acquisition. A
727
+ causally verified terminal automation failure can reconcile and release the
728
+ handoff; terminal settlement does not imply a successful ticket outcome. An
729
+ unexplained run failure still requires investigation.
730
+ Nontransactional reset/fork operations retain per-attempt admissions after an
731
+ uncertain mutating failure. **Those admissions currently require operator recovery
732
+ and are an explicit operability/release gate for affected runs:** a later retry or
733
+ current SQL outcome alone cannot prove an older provider call has stopped. No API
734
+ silently expires, impersonates, or clears those attempts. Review provider-specific
735
+ reconciliation before enabling affected-run recovery.
package/VERSION-PIN.md CHANGED
@@ -94,6 +94,21 @@ before publication; an ordinary released CLI intentionally cannot.
94
94
  The React/Vite starter therefore uses released primitives and responsive CSS
95
95
  until that pin advances. Its build test exercises the ordinary CLI toolchain.
96
96
 
97
+ ## Planned team directory and embedded-task release
98
+
99
+ Artifact Studio 0.10.0 and Studio CLI 0.6.6 are prepared for the separate
100
+ chain-release workflow and remain awaiting publication. This change adds
101
+ user-authorized directory/group methods, parked human-task assignment reads,
102
+ and a host-verified completion notification for embedded Next.js tasks. The
103
+ narrow app-access bridge reads only the signed-in viewer's app access.
104
+
105
+ Deploy the matching platform handlers before activating consumers. An SDK
106
+ build or candidate version is not evidence that the registry or target host
107
+ supports these methods. Group administration still requires authority over the
108
+ group; an application's manage capability does not grant it. Completion
109
+ notification verifies the host's own run/step and does not execute or replay a
110
+ workflow transition. Existing package export classifications stay unchanged.
111
+
97
112
  ## Unreleased server process additions
98
113
 
99
114
  Workspace source adds `processes.listRunsPage` (visible, paginated run summaries)
package/dist/api.d.ts CHANGED
@@ -19,6 +19,11 @@ export declare function postJson<T>({ baseUrl, token, path, body, retry503, sign
19
19
  signal?: AbortSignal;
20
20
  headers?: Record<string, string>;
21
21
  }): Promise<T>;
22
+ /** Binding updates are idempotent; normal authenticated target transport applies. */
23
+ export declare function putJson<T>({ baseUrl, token, path, body }: ApiClientOptions & {
24
+ path: string;
25
+ body: unknown;
26
+ }): Promise<T>;
22
27
  export declare function deleteJson({ baseUrl, token, path, signal }: ApiClientOptions & {
23
28
  path: string;
24
29
  signal?: AbortSignal;
package/dist/api.js CHANGED
@@ -111,6 +111,13 @@ export async function postJson({ baseUrl, token, path, body, retry503 = true, si
111
111
  throw new ArtifactApiError(await responseError('POST', path, response), response.status);
112
112
  return response.json();
113
113
  }
114
+ /** Binding updates are idempotent; normal authenticated target transport applies. */
115
+ export async function putJson({ baseUrl, token, path, body }) {
116
+ const response = await authenticatedFetch({ baseUrl, token, path, init: { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } });
117
+ if (!response.ok)
118
+ throw new ArtifactApiError(await responseError('PUT', path, response), response.status);
119
+ return response.json();
120
+ }
114
121
  export async function deleteJson({ baseUrl, token, path, signal }) {
115
122
  const response = await authenticatedFetch({ baseUrl, token, path, init: { method: 'DELETE', ...(signal ? { signal } : {}) } });
116
123
  if (!response.ok)
@@ -1,4 +1,4 @@
1
- /** Fail an artifact deploy whose source still points at another environment. */
1
+ /** Validate explicit artifact manifest bindings against the selected environment. */
2
2
  export declare function assertArtifactEnvironmentBindings({ directory, environment, token, }: {
3
3
  directory: string;
4
4
  environment: {
@@ -1,10 +1,11 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
1
3
  import { getJsonOr404 } from './api.js';
2
4
  import { knownEnvUrls } from './config.js';
3
5
  import { assertNoCrossEnvironmentBindings, canonicalDeploymentOrigin } from './env-bindings.js';
4
- import { readArtifactStudioSource } from './project.js';
5
- /** Fail an artifact deploy whose source still points at another environment. */
6
+ /** Validate explicit artifact manifest bindings against the selected environment. */
6
7
  export async function assertArtifactEnvironmentBindings({ directory, environment, token, }) {
7
- const source = await readArtifactStudioSource(directory);
8
+ const manifest = await readFile(join(directory, 'artifact.bundle.yml'), 'utf8');
8
9
  const deployOrigin = canonicalDeploymentOrigin(environment.url);
9
10
  if (!deployOrigin)
10
11
  throw new Error(`Invalid deployment URL '${environment.url}'.`);
@@ -17,7 +18,7 @@ export async function assertArtifactEnvironmentBindings({ directory, environment
17
18
  deployOrigin,
18
19
  origins,
19
20
  workspaces: [],
20
- files: source.files,
21
+ files: [{ path: 'artifact.bundle.yml', content: manifest }],
21
22
  workspaceExists: async (workspaceId) => {
22
23
  const workspace = await getJsonOr404({
23
24
  baseUrl: environment.url,
@@ -0,0 +1,8 @@
1
+ import { type Plugin } from 'vite';
2
+ import { type SourceBoundary } from './source-boundary.js';
3
+ export declare function guardStylesheetInputsPlugin(boundary: SourceBoundary): Plugin;
4
+ /**
5
+ * Reject `new URL('<path>', import.meta.url)` assets outside the boundary. A
6
+ * template literal is a glob over its static directory, which must stay inside.
7
+ */
8
+ export declare function guardImportMetaUrlPlugin(boundary: SourceBoundary): Plugin;
@@ -0,0 +1,178 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { createIdResolver } from 'vite';
5
+ import { isBareImport } from './source-boundary.js';
6
+ /**
7
+ * Vite and Tailwind resolve some build inputs with internal resolvers that
8
+ * never reach a plugin's `resolveId`: stylesheet `@import`, CSS `url()` and
9
+ * `image-set()`, and `new URL('<literal>', import.meta.url)`. Tailwind also
10
+ * runs JavaScript named by `@plugin` and `@config`. The build runs on the
11
+ * caller's machine (a laptop, CI, or an agent sandbox holding the caller's
12
+ * token), so these guards check every such reference against the source
13
+ * boundary before Vite or Tailwind acts on it.
14
+ */
15
+ /** The stylesheet requests Vite and Tailwind process (Vite's own CSS language test). */
16
+ const STYLESHEET_REQUEST_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)|[?&]lang\.css\b/;
17
+ /** Tailwind at-rules that load and run JavaScript at build time. */
18
+ const BUILD_CODE_AT_RULE_RE = /@(?:plugin|config)(?![\w-])/i;
19
+ /** A CSS string: either quote, with the other quote and escapes allowed inside. */
20
+ const CSS_STRING = String.raw `"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'`;
21
+ /** `@import` / `@reference` targets: a string, or a string or bare token in `url()`. */
22
+ const STYLESHEET_IMPORT_RE = new RegExp(String.raw `@(?:import|reference)\s*(?:url\(\s*)?(?:${CSS_STRING}|([^'"\s;)]+))`, 'gi');
23
+ /** Tailwind `@source` paths (`@source inline(...)` names classes, not files). */
24
+ const SOURCE_DIRECTIVE_RE = new RegExp(String.raw `@source\s+(?:not\s+)?(?:${CSS_STRING})`, 'gi');
25
+ /** Mirrors Vite's `cssUrlRE`, so every `url()` Vite rewrites is checked. */
26
+ const CSS_URL_RE = /(?<=^|[^\w\-\u0080-￿])url\((\s*('[^']+'|"[^"]+")\s*|(?:\\.|[^'")\\])+)\)/g;
27
+ const IMAGE_SET_RE = /image-set\(([^)]*(?:\([^)]*\)[^)]*)*)\)/gi;
28
+ const QUOTED_RE = new RegExp(CSS_STRING, 'g');
29
+ /** Mirrors Vite's `assetImportMetaUrlRE`. */
30
+ const IMPORT_META_URL_RE = /\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/g;
31
+ const NON_FILE_URL_RE = /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i;
32
+ function pathOf(id) {
33
+ if (id.startsWith('\0'))
34
+ return null;
35
+ const filePath = id.split('?')[0];
36
+ return isAbsolute(filePath) ? filePath : null;
37
+ }
38
+ /**
39
+ * Where Vite would read a referenced asset from: relative to the referencing
40
+ * file, root-relative then absolute for a leading `/`. Null for a URL that is
41
+ * not a local file or that does not exist (Vite leaves those unresolved).
42
+ */
43
+ function referencedFile({ reference, importer, root }) {
44
+ let target = reference.trim().replace(/^['"]|['"]$/g, '').split(/[?#]/)[0];
45
+ try {
46
+ target = decodeURI(target);
47
+ }
48
+ catch {
49
+ // Vite keeps an undecodable URL as written.
50
+ }
51
+ if (!target || NON_FILE_URL_RE.test(target))
52
+ return null;
53
+ const candidates = target.startsWith('/') ? [join(root, target), target] : [resolve(dirname(importer), target)];
54
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
55
+ }
56
+ /**
57
+ * Reject Tailwind JavaScript loading and out-of-boundary references in every
58
+ * stylesheet the build processes, following `@import` into nested stylesheets
59
+ * because Tailwind and Vite read those without another plugin pass. Must run
60
+ * before Tailwind: list it ahead of `@tailwindcss/vite`.
61
+ */
62
+ /** The first capture group a match filled: the string body or the bare token. */
63
+ function captured(match) {
64
+ return match.slice(1).find((group) => group !== undefined);
65
+ }
66
+ export function guardStylesheetInputsPlugin(boundary) {
67
+ // Files already scanned or being scanned. Recording a file before awaiting
68
+ // its imports makes an import cycle end instead of awaiting an ancestor.
69
+ const started = new Set();
70
+ // Resolve package stylesheets as Vite's CSS @import does (the `style`
71
+ // condition): a CSS-only package such as tw-animate-css has no JS entry.
72
+ let resolveStylesheet;
73
+ async function checkStylesheet({ file, code, resolveStylesheetImport }) {
74
+ if (!boundary.isTrusted(file) && BUILD_CODE_AT_RULE_RE.test(code)) {
75
+ throw new Error(`Artifact stylesheet "${file}" uses Tailwind @plugin or @config, which runs JavaScript on the build machine. ` +
76
+ 'Configure Tailwind in CSS (@theme, @utility, @custom-variant) instead.');
77
+ }
78
+ const assertPath = (reference, kind) => {
79
+ const filePath = referencedFile({ reference, importer: file, root: boundary.root });
80
+ if (filePath)
81
+ boundary.assertBuildInput({ filePath, importer: file, bareSpecifier: false, reference: kind });
82
+ return filePath;
83
+ };
84
+ const nested = [];
85
+ for (const match of code.matchAll(STYLESHEET_IMPORT_RE)) {
86
+ const specifier = captured(match);
87
+ if (!specifier || NON_FILE_URL_RE.test(specifier))
88
+ continue;
89
+ // Check both the literal path and what the CSS resolver finds (which adds
90
+ // `.css` to an extensionless path and follows a package's `style` entry):
91
+ // Tailwind and Vite load whichever exists.
92
+ const literal = assertPath(specifier, 'stylesheet import');
93
+ const resolved = await resolveStylesheetImport(specifier, file);
94
+ if (resolved) {
95
+ boundary.assertBuildInput({ filePath: resolved, importer: file, bareSpecifier: isBareImport(specifier), reference: 'stylesheet import' });
96
+ }
97
+ // Tailwind and Vite parse any imported file as CSS, whatever its extension.
98
+ for (const filePath of new Set([literal, resolved]))
99
+ if (filePath)
100
+ nested.push(filePath);
101
+ }
102
+ for (const match of code.matchAll(SOURCE_DIRECTIVE_RE)) {
103
+ const path = captured(match);
104
+ if (path)
105
+ boundary.assertBuildInput({ filePath: resolve(dirname(file), path), importer: file, bareSpecifier: false, reference: '@source path' });
106
+ }
107
+ for (const [, reference] of code.matchAll(CSS_URL_RE)) {
108
+ if (reference)
109
+ assertPath(reference, 'stylesheet url()');
110
+ }
111
+ for (const [, body] of code.matchAll(IMAGE_SET_RE)) {
112
+ for (const quoted of (body ?? '').matchAll(QUOTED_RE)) {
113
+ const reference = captured(quoted);
114
+ if (reference)
115
+ assertPath(reference, 'stylesheet image-set()');
116
+ }
117
+ }
118
+ await Promise.all(nested.map((nestedFile) => checkFile(nestedFile, resolveStylesheetImport)));
119
+ }
120
+ async function checkFile(file, resolveStylesheetImport) {
121
+ // A file another traversal already started is checked there, and a failure
122
+ // there fails the build.
123
+ if (started.has(file))
124
+ return;
125
+ started.add(file);
126
+ await checkStylesheet({ file, code: await readFile(file, 'utf8'), resolveStylesheetImport });
127
+ }
128
+ return {
129
+ name: 'guard-artifact-stylesheet-inputs',
130
+ enforce: 'pre',
131
+ configResolved(config) {
132
+ resolveStylesheet = createIdResolver(config, {
133
+ extensions: ['.css'], mainFields: ['style'], conditions: ['style', 'production'], tryIndex: false, preferRelative: true,
134
+ });
135
+ },
136
+ async transform(code, id) {
137
+ const file = pathOf(id);
138
+ if (!file || !STYLESHEET_REQUEST_RE.test(id) || /[?&]raw\b/.test(id))
139
+ return null;
140
+ const environment = this.environment;
141
+ const resolveStylesheetImport = async (specifier, importer) => {
142
+ // An import the CSS resolver cannot find is checked by its literal path;
143
+ // anything else unresolvable fails in Vite or Tailwind as well.
144
+ const resolved = await resolveStylesheet?.(environment, specifier, importer).catch(() => undefined);
145
+ return resolved ? pathOf(resolved) : null;
146
+ };
147
+ await checkStylesheet({ file, code, resolveStylesheetImport });
148
+ return null;
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * Reject `new URL('<path>', import.meta.url)` assets outside the boundary. A
154
+ * template literal is a glob over its static directory, which must stay inside.
155
+ */
156
+ export function guardImportMetaUrlPlugin(boundary) {
157
+ return {
158
+ name: 'guard-artifact-import-meta-url',
159
+ enforce: 'pre',
160
+ transform(code, id) {
161
+ const file = pathOf(id);
162
+ if (!file || STYLESHEET_REQUEST_RE.test(id) || !code.includes('import.meta.url'))
163
+ return null;
164
+ for (const [, literal] of code.matchAll(IMPORT_META_URL_RE)) {
165
+ const body = literal.slice(1, -1);
166
+ const isTemplate = literal.startsWith('`') && body.includes('${');
167
+ const reference = isTemplate ? body.slice(0, body.lastIndexOf('/', body.indexOf('${')) + 1) || '.' : body;
168
+ if (NON_FILE_URL_RE.test(reference))
169
+ continue;
170
+ const target = reference.startsWith('/')
171
+ ? [join(boundary.root, reference), reference].find((candidate) => existsSync(candidate)) ?? join(boundary.root, reference)
172
+ : resolve(dirname(file), reference);
173
+ boundary.assertBuildInput({ filePath: target, importer: file, bareSpecifier: false, reference: 'new URL() asset' });
174
+ }
175
+ return null;
176
+ },
177
+ };
178
+ }
package/dist/build.js CHANGED
@@ -15,7 +15,8 @@ import { isNextArtifactManifest } from './manifest.js';
15
15
  import { normalizeArtifactPath } from './paths.js';
16
16
  import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
17
17
  import { ARTIFACT_RUNTIME_CHUNK_DIR, assertSafeRuntimeChunkFileName } from './runtime-chunks.js';
18
- import { rejectOutsideSourcePlugin } from './source-boundary.js';
18
+ import { guardImportMetaUrlPlugin, guardStylesheetInputsPlugin } from './build-input-guards.js';
19
+ import { createSourceBoundary, rejectOutsideSourcePlugin } from './source-boundary.js';
19
20
  import { collectCandidateTokenCompatibilityDiagnostics, collectDeprecatedTokenImportDiagnostics, } from './token-contract.js';
20
21
  /** Keep in sync with `atlas/src/server/services/artifact-studio/limits.ts`. */
21
22
  const MAX_DEPLOY_SOURCEMAP_BYTES = 10 * 1024 * 1024;
@@ -126,16 +127,24 @@ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompat
126
127
  // Give Vite and the source boundary the same physical root.
127
128
  const root = await realpath(source.root);
128
129
  const transformedCssModules = new Map();
130
+ const boundary = createSourceBoundary({
131
+ root,
132
+ trustedExternalRoots: [...Object.values(peerResolveAlias).map((target) => dirname(target)), dirname(artifactSdkEntry)],
133
+ });
129
134
  const result = await viteBuild({
130
135
  root,
131
136
  configFile: false,
132
137
  logLevel: 'silent',
138
+ // An inline PostCSS config stops Vite from loading postcss.config.* (or
139
+ // .postcssrc) from the source tree, which would run repository code on the
140
+ // build machine. Tailwind runs as the Vite plugin below and needs none.
141
+ css: { postcss: { plugins: [] } },
133
142
  plugins: [
134
- rejectOutsideSourcePlugin({
135
- root,
136
- trustedExternalRoots: [...Object.values(peerResolveAlias).map((target) => dirname(target)), dirname(artifactSdkEntry)],
137
- }),
143
+ rejectOutsideSourcePlugin(boundary),
138
144
  rejectIgnoredSourcePlugin(root, source.ignorePatterns),
145
+ // Before Tailwind, which reads @import / @plugin / @config itself.
146
+ guardStylesheetInputsPlugin(boundary),
147
+ guardImportMetaUrlPlugin(boundary),
139
148
  artifactSdkVirtualModule(),
140
149
  atlasUiCssResolverPlugin({
141
150
  atlasUiDir: selectedAtlasUiDir,
@@ -0,0 +1,8 @@
1
+ /** Public CLI for reviewed target bindings; never creates provider registrations. */
2
+ export declare function runContentReadersCommand(input: {
3
+ action: string;
4
+ projectId?: string | true;
5
+ file?: string | true;
6
+ baseUrl: string;
7
+ token: string;
8
+ }): Promise<number>;
@@ -0,0 +1,28 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { z } from 'zod';
3
+ import { getJson, putJson } from './api.js';
4
+ const bindings = z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), z.strictObject({ enabled: z.boolean(), revision: z.string().regex(/^[a-f0-9]{64}$/) }))
5
+ .refine(value => Object.keys(value).length >= 1 && Object.keys(value).length <= 16);
6
+ /** Public CLI for reviewed target bindings; never creates provider registrations. */
7
+ export async function runContentReadersCommand(input) {
8
+ const projectId = z.uuid().parse(input.projectId);
9
+ if (input.action !== 'show' && input.action !== 'apply')
10
+ throw new Error('content-readers requires show or apply');
11
+ if (input.action === 'show' && input.file !== undefined)
12
+ throw new Error('show does not accept --file');
13
+ const path = `/api/artifact-studio/projects/${projectId}/next/content-readers`;
14
+ if (input.action === 'show') {
15
+ console.log(JSON.stringify(await getJson({ baseUrl: input.baseUrl, token: input.token, path }), null, 2));
16
+ return 0;
17
+ }
18
+ if (typeof input.file !== 'string' || !input.file)
19
+ throw new Error('apply requires --file with reviewed bindings');
20
+ if ((await stat(input.file)).size > 65536)
21
+ throw new Error('Content-reader binding file exceeds 64 KiB');
22
+ const raw = await readFile(input.file, 'utf8');
23
+ if (Buffer.byteLength(raw) > 65536)
24
+ throw new Error('Content-reader binding file exceeds 64 KiB');
25
+ const body = bindings.parse(JSON.parse(raw));
26
+ console.log(JSON.stringify(await putJson({ baseUrl: input.baseUrl, token: input.token, path, body }), null, 2));
27
+ return 0;
28
+ }
package/dist/cli.d.ts CHANGED
@@ -39,5 +39,6 @@ export interface ProvidedToken {
39
39
  authMode: SourceAuthMode;
40
40
  token: string;
41
41
  }
42
+ export declare function setPackageInstallUnavailable(guidance: string | null): void;
42
43
  export declare function setTokenProvider(provider: ((options: TokenProviderOptions) => Promise<ProvidedToken | null>) | null): void;
43
44
  export {};