@yargote/artifacts-client 0.0.0-stage → 0.1.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yerson Argote
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,65 @@
1
- # Temporary Holding Version
1
+ # Artifacts HTTP client reference
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@yargote/artifacts-client` is a typed Fetch client for the Artifacts REST API. It depends on `@yargote/artifacts-contracts` and Zod. It does not install the artifact store or a database driver. Node 24 or later is required.
4
+
5
+ ```sh
6
+ npm install @yargote/artifacts-client@0.1.0-beta.1
7
+ ```
8
+
9
+ ```ts
10
+ import { createArtifactHttpClient, HttpError } from '@yargote/artifacts-client';
11
+
12
+ const client = createArtifactHttpClient({
13
+ resource: process.env.ARTIFACTS_SERVER!,
14
+ accessToken: async () => obtainAccessToken(),
15
+ timeoutMs: 30_000,
16
+ });
17
+
18
+ const identity = await client.identity();
19
+ const document = await client.read({ artifactId: 'artifact-id' });
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ `createArtifactHttpClient({ resource, accessToken, fetch?, timeoutMs? })` returns an `ArtifactHttpClient`.
25
+
26
+ `resource` is the exact canonical OAuth resource. A resource ending in `/mcp` uses REST routes under `/mcp/v1`. HTTPS is required except for HTTP on `localhost`, `127.0.0.1`, or `[::1]`. Credentials, queries, fragments, and noncanonical URLs are rejected. The client has no default resource.
27
+
28
+ `accessToken` is a bearer token or an asynchronous function that returns one. The function runs once per method call. The caller owns credential storage and refresh.
29
+
30
+ `fetch` defaults to `globalThis.fetch`. `timeoutMs` defaults to 30 seconds and covers the request and response body. It must be a positive safe integer no greater than 2147483647.
31
+
32
+ ## Methods
33
+
34
+ Every method makes one HTTP request. The client does not retry, follow redirects, rebase conflicts, or change expected versions.
35
+
36
+ | Method | Input | Result |
37
+ | --- | --- | --- |
38
+ | `identity()` | None | Verified user, session, client, and scopes |
39
+ | `create(command)` | `requestId`, complete `snapshot`, optional `response` | Artifact receipt |
40
+ | `list(query?)` | Optional `limit` and `cursor` | Artifact summaries and `nextCursor` |
41
+ | `read(query)` | `artifactId`, optional `revisionId` and `response` | Artifact revision |
42
+ | `update(command)` | `requestId`, `artifactId`, `expectedRevisionId`, complete `snapshot`, optional `response` | Artifact receipt |
43
+ | `history(query)` | `artifactId`, optional `limit`, `beforeSequence`, and `response` | Revisions and `nextBeforeSequence` |
44
+ | `publish(command)` | `requestId`, `artifactId`, `revisionId`, `expectedPublicationVersion` | Publication receipt |
45
+ | `export(query)` | `artifactId`, `revisionId` | `{ filename, mediaType, bytes, sha256 }` |
46
+
47
+ Create, read, and update accept `response: 'compact'`. The returned revision contains metadata and file summaries. Omitted response and `response: 'full'` return the complete snapshot. History accepts `response: 'summary'` or `response: 'full'`. Its page limit is ten revisions. Overloads reflect these projections in the return type.
48
+
49
+ All commands and JSON responses pass the strict domain schemas. An omitted mutation `response` remains omitted in the transmitted command. JSON responses are bounded at 64 MiB, which accommodates ten maximum-size revisions with JSON escaping. Archives are bounded at 8 MiB. Export verifies the binary bytes against `X-Artifact-SHA256` and returns the bytes without rebuilding the ZIP.
50
+
51
+ `prepareArtifactEdit(client, { artifactId, requestId, changes })` reads the current full snapshot and prepares an immutable update command. Metadata and content changes merge into that snapshot. An explicit `files` array replaces the attachments. The helper does not submit the update. Retain its exact output before sending it.
52
+
53
+ ## Errors and retries
54
+
55
+ `HttpError` extends `Error` and contains `status`, `code`, and `details`. Server conflicts preserve the server's details. Transport failures use status `0` and code `network_error` or `timeout`. Rejected redirects use `redirect_refused`. Invalid response schemas use `invalid_response`. Oversized responses use `response_too_large`. A mismatched archive digest uses `archive_digest_mismatch`.
56
+
57
+ Invalid local commands throw a Zod validation error before any HTTP request. Invalid configuration throws `TypeError`.
58
+
59
+ A caller that needs recovery retains the complete create, update, or publish command before its first request. Retrying that exact command returns the original receipt. Changing `response`, the snapshot, or any expected version changes the command. A conflict requires a new deliberate command with a new request ID. A replayed receipt describes the original operation and can differ from a fresh read.
60
+
61
+ ## Compiled installation
62
+
63
+ The registry package contains compiled ESM JavaScript and declarations. Consumers need no TypeScript loader or database package. For repository development, `pnpm build:packages` produces local tarballs and a `dist/packages/manifest.json` inventory. `pnpm test:packages` verifies an ordinary Node consumer and its types.
64
+
65
+ This package is MIT licensed. The license applies to this package and does not license the private Artifacts application repository.
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Typed HTTP client for the Artifacts REST API.",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/yersonargotev/artifacts.git",
11
+ "directory": "packages/client"
12
+ },
13
+ "homepage": "https://github.com/yersonargotev",
14
+ "bugs": {
15
+ "url": "https://github.com/yersonargotev"
16
+ },
17
+ "engines": {
18
+ "node": ">=24"
19
+ },
20
+ "exports": {
21
+ ".": "./src/index.js"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "dependencies": {
29
+ "@yargote/artifacts-contracts": "0.1.0-beta.1",
30
+ "zod": "4.6.5"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "tag": "beta",
35
+ "provenance": false
36
+ }
37
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ import { createSchema, exportSchema, historySchema, listSchema, publishResultSchema, publishSchema, readSchema, type ArtifactRead, type UpdateInput } from '@yargote/artifacts-contracts';
3
+ import { historyResultSchema, listResultSchema, summaryHistoryResultSchema, type Identity } from '@yargote/artifacts-contracts/http';
4
+ import type { CompactArtifactRead } from '@yargote/artifacts-contracts/workflow';
5
+ export type { ArtifactRead, Snapshot, UpdateInput } from '@yargote/artifacts-contracts';
6
+ export type { Identity } from '@yargote/artifacts-contracts/http';
7
+ export type { CompactArtifactRead, RevisionSummary } from '@yargote/artifacts-contracts/workflow';
8
+ export { prepareArtifactEdit } from '@yargote/artifacts-contracts/workflow';
9
+ export type CreateInput = z.input<typeof createSchema>;
10
+ export type ReadInput = z.input<typeof readSchema>;
11
+ export type PublishInput = z.input<typeof publishSchema>;
12
+ export type ListInput = z.input<typeof listSchema>;
13
+ export type HistoryInput = z.input<typeof historySchema>;
14
+ export type ExportInput = z.input<typeof exportSchema>;
15
+ export type ArtifactArchive = {
16
+ filename: string;
17
+ mediaType: 'application/zip';
18
+ bytes: Uint8Array;
19
+ sha256: string;
20
+ };
21
+ export type HistoryResult = z.infer<typeof historyResultSchema>;
22
+ export type SummaryHistoryResult = z.infer<typeof summaryHistoryResultSchema>;
23
+ export declare class HttpError extends Error {
24
+ readonly status: number;
25
+ readonly code: string;
26
+ readonly details: Record<string, unknown>;
27
+ constructor(status: number, code: string, message: string, details?: Record<string, unknown>, options?: ErrorOptions);
28
+ }
29
+ export interface ArtifactHttpClient {
30
+ identity(): Promise<Identity>;
31
+ create(input: CreateInput & {
32
+ response: 'compact';
33
+ }): Promise<CompactArtifactRead>;
34
+ create(input: CreateInput & {
35
+ response?: 'full';
36
+ }): Promise<ArtifactRead>;
37
+ create(input: CreateInput): Promise<ArtifactRead | CompactArtifactRead>;
38
+ list(input?: ListInput): Promise<z.infer<typeof listResultSchema>>;
39
+ read(input: ReadInput & {
40
+ response: 'compact';
41
+ }): Promise<CompactArtifactRead>;
42
+ read(input: ReadInput & {
43
+ response?: 'full';
44
+ }): Promise<ArtifactRead>;
45
+ read(input: ReadInput): Promise<ArtifactRead | CompactArtifactRead>;
46
+ update(input: UpdateInput & {
47
+ response: 'compact';
48
+ }): Promise<CompactArtifactRead>;
49
+ update(input: UpdateInput & {
50
+ response?: 'full';
51
+ }): Promise<ArtifactRead>;
52
+ update(input: UpdateInput): Promise<ArtifactRead | CompactArtifactRead>;
53
+ history(input: HistoryInput & {
54
+ response: 'summary';
55
+ }): Promise<SummaryHistoryResult>;
56
+ history(input: HistoryInput & {
57
+ response?: 'full';
58
+ }): Promise<HistoryResult>;
59
+ history(input: HistoryInput): Promise<HistoryResult | SummaryHistoryResult>;
60
+ publish(input: PublishInput): Promise<z.infer<typeof publishResultSchema>>;
61
+ export(input: ExportInput): Promise<ArtifactArchive>;
62
+ }
63
+ export declare function createArtifactHttpClient(options: {
64
+ resource: string;
65
+ accessToken: string | (() => Promise<string>);
66
+ fetch?: typeof globalThis.fetch;
67
+ timeoutMs?: number;
68
+ }): ArtifactHttpClient;
package/src/index.js ADDED
@@ -0,0 +1,175 @@
1
+ import { z } from 'zod';
2
+ import { artifactReadSchema, createSchema, exportSchema, historySchema, listSchema, publishResultSchema, publishSchema, readSchema, updateSchema } from '@yargote/artifacts-contracts';
3
+ import { compactArtifactReadSchema, historyResultSchema, httpErrorSchema, identitySchema, listResultSchema, restBase, summaryHistoryResultSchema } from '@yargote/artifacts-contracts/http';
4
+ export { prepareArtifactEdit } from '@yargote/artifacts-contracts/workflow';
5
+ export class HttpError extends Error {
6
+ status;
7
+ code;
8
+ details;
9
+ constructor(status, code, message, details = {}, options) {
10
+ super(message, options);
11
+ this.name = 'HttpError';
12
+ this.status = status;
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+ const maxJsonBytes = 64 * 1024 * 1024;
18
+ const maxArchiveBytes = 8 * 1024 * 1024;
19
+ async function boundedBytes(response, maximum) {
20
+ const length = response.headers.get('content-length');
21
+ if (length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
22
+ await response.body?.cancel();
23
+ throw new HttpError(response.status, 'response_too_large', 'The server response exceeds the size limit');
24
+ }
25
+ const reader = response.body?.getReader();
26
+ if (!reader)
27
+ return new Uint8Array();
28
+ const chunks = [];
29
+ let size = 0;
30
+ try {
31
+ while (true) {
32
+ const next = await reader.read();
33
+ if (next.done)
34
+ break;
35
+ size += next.value.byteLength;
36
+ if (size > maximum) {
37
+ await reader.cancel();
38
+ throw new HttpError(response.status, 'response_too_large', 'The server response exceeds the size limit');
39
+ }
40
+ chunks.push(next.value);
41
+ }
42
+ }
43
+ finally {
44
+ reader.releaseLock();
45
+ }
46
+ const bytes = new Uint8Array(size);
47
+ let offset = 0;
48
+ for (const chunk of chunks) {
49
+ bytes.set(chunk, offset);
50
+ offset += chunk.byteLength;
51
+ }
52
+ return bytes;
53
+ }
54
+ function query(values) {
55
+ const parameters = new URLSearchParams();
56
+ for (const [key, value] of Object.entries(values))
57
+ if (value !== undefined)
58
+ parameters.set(key, String(value));
59
+ const encoded = parameters.toString();
60
+ return encoded ? `?${encoded}` : '';
61
+ }
62
+ function parseResponse(schema, value, status) {
63
+ const result = schema.safeParse(value);
64
+ if (!result.success)
65
+ throw new HttpError(status, 'invalid_response', 'The server returned an invalid response');
66
+ return result.data;
67
+ }
68
+ export function createArtifactHttpClient(options) {
69
+ const base = restBase(options.resource);
70
+ const fetcher = options.fetch ?? globalThis.fetch;
71
+ const timeoutMs = options.timeoutMs ?? 30_000;
72
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
73
+ throw new TypeError('timeoutMs must be a positive integer no greater than 2147483647');
74
+ async function request(path, method, parse, input, maximum = maxJsonBytes) {
75
+ const token = typeof options.accessToken === 'string' ? options.accessToken : await options.accessToken();
76
+ if (!token || /[\r\n]/.test(token))
77
+ throw new TypeError('A nonempty access token is required');
78
+ const controller = new AbortController();
79
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
80
+ try {
81
+ const response = await fetcher(`${base}${path}`, {
82
+ method,
83
+ redirect: 'manual',
84
+ signal: controller.signal,
85
+ headers: { authorization: `Bearer ${token}`, accept: maximum === maxArchiveBytes ? 'application/zip, application/json' : 'application/json', ...(input === undefined ? {} : { 'content-type': 'application/json' }) },
86
+ ...(input === undefined ? {} : { body: JSON.stringify(input) }),
87
+ });
88
+ if (response.status >= 300 && response.status < 400) {
89
+ await response.body?.cancel();
90
+ throw new HttpError(response.status, 'redirect_refused', 'The server attempted to redirect an authenticated request');
91
+ }
92
+ const bytes = await boundedBytes(response, response.ok ? maximum : Math.min(maximum, 1024 * 1024));
93
+ if (!response.ok) {
94
+ let value;
95
+ try {
96
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
97
+ }
98
+ catch {
99
+ throw new HttpError(response.status, 'invalid_response', 'The server returned an invalid error response');
100
+ }
101
+ const { error } = parseResponse(httpErrorSchema, value, response.status);
102
+ throw new HttpError(response.status, error.code, error.message, error.details);
103
+ }
104
+ const expectedStatus = method === 'POST' ? 201 : 200;
105
+ if (response.status !== expectedStatus)
106
+ throw new HttpError(response.status, 'invalid_response', `The server returned status ${response.status}, expected ${expectedStatus}`);
107
+ return await parse(response, bytes);
108
+ }
109
+ catch (error) {
110
+ if (error instanceof HttpError)
111
+ throw error;
112
+ if (controller.signal.aborted)
113
+ throw new HttpError(0, 'timeout', 'The server request timed out', {}, { cause: error });
114
+ throw new HttpError(0, 'network_error', 'The server request failed', {}, { cause: error });
115
+ }
116
+ finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
120
+ function json(path, method, schema, input) {
121
+ return request(path, method, (response, bytes) => {
122
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json')
123
+ throw new HttpError(response.status, 'invalid_response', 'The server did not return JSON');
124
+ let value;
125
+ try {
126
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
127
+ }
128
+ catch {
129
+ throw new HttpError(response.status, 'invalid_response', 'The server returned invalid JSON');
130
+ }
131
+ return parseResponse(schema, value, response.status);
132
+ }, input);
133
+ }
134
+ const client = {
135
+ identity: () => json('/identity', 'GET', identitySchema),
136
+ create(value) {
137
+ const input = createSchema.parse(value);
138
+ return json('/artifacts', 'POST', input.response === 'compact' ? compactArtifactReadSchema : artifactReadSchema, input);
139
+ },
140
+ list(value = {}) {
141
+ return json(`/artifacts${query(listSchema.parse(value))}`, 'GET', listResultSchema);
142
+ },
143
+ read(value) {
144
+ const { artifactId, ...parameters } = readSchema.parse(value);
145
+ return json(`/artifacts/${encodeURIComponent(artifactId)}${query(parameters)}`, 'GET', parameters.response === 'compact' ? compactArtifactReadSchema : artifactReadSchema);
146
+ },
147
+ update(value) {
148
+ const input = updateSchema.parse(value);
149
+ return json(`/artifacts/${encodeURIComponent(input.artifactId)}`, 'PUT', input.response === 'compact' ? compactArtifactReadSchema : artifactReadSchema, input);
150
+ },
151
+ history(value) {
152
+ const { artifactId, ...parameters } = historySchema.parse(value);
153
+ return json(`/artifacts/${encodeURIComponent(artifactId)}/history${query(parameters)}`, 'GET', parameters.response === 'summary' ? summaryHistoryResultSchema : historyResultSchema);
154
+ },
155
+ publish(value) {
156
+ const input = publishSchema.parse(value);
157
+ return json(`/artifacts/${encodeURIComponent(input.artifactId)}/publication`, 'PUT', publishResultSchema, input);
158
+ },
159
+ export(value) {
160
+ const input = exportSchema.parse(value);
161
+ return request(`/artifacts/${encodeURIComponent(input.artifactId)}/revisions/${encodeURIComponent(input.revisionId)}/export`, 'GET', async (response, bytes) => {
162
+ const disposition = response.headers.get('content-disposition');
163
+ const filename = /^attachment; filename="([A-Za-z0-9._-]+\.zip)"$/.exec(disposition ?? '')?.[1];
164
+ const digest = response.headers.get('x-artifact-sha256');
165
+ if (!filename || !digest || !/^[a-f0-9]{64}$/.test(digest) || response.headers.get('content-type')?.split(';')[0].trim() !== 'application/zip')
166
+ throw new HttpError(response.status, 'invalid_response', 'The server returned invalid archive headers');
167
+ const actualDigest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(bytes))), (byte) => byte.toString(16).padStart(2, '0')).join('');
168
+ if (actualDigest !== digest)
169
+ throw new HttpError(response.status, 'archive_digest_mismatch', 'The archive SHA-256 does not match its response header');
170
+ return { filename, mediaType: 'application/zip', bytes, sha256: digest };
171
+ }, undefined, maxArchiveBytes);
172
+ },
173
+ };
174
+ return client;
175
+ }