@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.
- package/README.md +219 -0
- package/VERSION-PIN.md +15 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.js +7 -0
- package/dist/artifact-env-bindings.d.ts +1 -1
- package/dist/artifact-env-bindings.js +5 -4
- package/dist/build-input-guards.d.ts +8 -0
- package/dist/build-input-guards.js +178 -0
- package/dist/build.js +14 -5
- package/dist/cli-content-readers.d.ts +8 -0
- package/dist/cli-content-readers.js +28 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +38 -6
- package/dist/{env-binding-scan.d.ts → env-binding-manifest.d.ts} +1 -4
- package/dist/env-binding-manifest.js +56 -0
- package/dist/env-bindings.js +2 -34
- package/dist/git-service-client.d.ts +11 -0
- package/dist/git-service-client.js +10 -0
- package/dist/manifest-next.d.ts +6 -0
- package/dist/manifest-next.js +3 -0
- package/dist/manifest.d.ts +4 -0
- package/dist/manifest.js +1 -0
- package/dist/next/bridge.d.ts +15 -1
- package/dist/next/bridge.js +18 -1
- package/dist/next-source-includes.d.ts +50 -0
- package/dist/next-source-includes.js +68 -0
- package/dist/prepare-build.d.ts +10 -2
- package/dist/prepare-build.js +33 -3
- package/dist/repository-identity.d.ts +31 -0
- package/dist/repository-identity.js +41 -0
- package/dist/runtime-contracts.d.ts +1 -4
- package/dist/runtime-contracts.js +2 -15
- package/dist/server/api-client.d.ts +1 -0
- package/dist/server/api-client.js +13 -7
- package/dist/server/group-directory.d.ts +90 -0
- package/dist/server/group-directory.js +17 -0
- package/dist/server/groups.d.ts +2 -1
- package/dist/server/groups.js +2 -0
- package/dist/server/human-completion.d.ts +44 -0
- package/dist/server/human-completion.js +38 -0
- package/dist/server/processes.d.ts +19 -0
- package/dist/server/processes.js +11 -0
- package/dist/server/task-assignments.d.ts +81 -0
- package/dist/server/task-assignments.js +26 -0
- package/dist/server.d.ts +4 -0
- package/dist/source-boundary.d.ts +27 -6
- package/dist/source-boundary.js +44 -30
- package/dist/source-exclusions.d.ts +4 -0
- package/dist/source-exclusions.js +16 -0
- package/dist/source-resolver.js +2 -1
- package/dist/templates/react-vite/package.json +1 -1
- package/package.json +6 -2
- package/templates/react-vite/package.json +1 -1
- 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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 {
|
|
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 {};
|