@yargote/artifacts-client 0.1.0-beta.5 → 0.1.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
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
4
 
5
5
  ```sh
6
- npm install @yargote/artifacts-client@0.1.0-beta.5
6
+ npm install @yargote/artifacts-client@0.1.0-beta.6
7
7
  ```
8
8
 
9
9
  ```ts
@@ -43,6 +43,9 @@ Every method makes one HTTP request. The client does not retry, follow redirects
43
43
  | `update(command)` | `requestId`, `artifactId`, `expectedRevisionId`, complete `snapshot`, optional `response` | Artifact receipt |
44
44
  | `history(query)` | `artifactId`, optional `limit`, `beforeSequence`, and `response` | Revisions and `nextBeforeSequence` |
45
45
  | `publish(command)` | `requestId`, `artifactId`, `revisionId`, `expectedPublicationVersion` | Publication receipt |
46
+ | `share(command)` | `requestId`, `artifactId`, `email`, optional `notify` and `requireNew: true` | Share receipt with optional notification disposition |
47
+ | `listShares(query)` | `artifactId` | `{ items: Share[] }` |
48
+ | `unshare(command)` | `requestId`, `artifactId`, `shareId` | `void` after an empty HTTP 204 |
46
49
  | `move(command)` | `requestId`, `artifactId`, `project` name or null | `{ artifactId, project }` |
47
50
  | `export(query)` | `artifactId`, `revisionId` | `{ filename, mediaType, bytes, sha256 }` |
48
51
 
@@ -50,6 +53,8 @@ Create, read, and update accept `response: 'compact'`. The returned revision con
50
53
 
51
54
  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.
52
55
 
56
+ All sharing methods require `artifacts:publish` and artifact ownership. `share` trims and lowercases the email. The client places identifiers in encoded URL segments and sends `{ requestId, email }` plus any explicitly supplied `notify` or `requireNew` for share or `{ requestId }` for unshare. `unshare` resolves with `undefined` and requires no JSON content type on its empty response.
57
+
53
58
  Artifact reads and list items include `project`, a name or null. A project name matches an existing project regardless of letter case and keeps its stored spelling. `move` never creates a revision or changes the publication. `projects()` lists only projects that contain artifacts, ordered by name. Because response schemas are strict, this client requires a server that returns `project`. Version `0.1.0-beta.4` and earlier reject responses from such a server with `invalid_response`.
54
59
 
55
60
  `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.
@@ -60,7 +65,7 @@ Artifact reads and list items include `project`, a name or null. A project name
60
65
 
61
66
  Invalid local commands throw a Zod validation error before any HTTP request. Invalid configuration throws `TypeError`.
62
67
 
63
- A caller that needs recovery retains the complete create, update, publish, or move command before its first request. While the artifact exists, 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.
68
+ A caller that needs recovery retains the complete create, update, publish, move, share, or unshare command before its first request. While the artifact exists, retrying that exact command returns the original receipt or confirms unshare with another empty 204. Changing `response`, the snapshot, an expected version, or a sharing argument 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. Replaying a share after revocation returns its historical receipt without restoring access. A new unshare command for a removed share returns `not_found`.
64
69
 
65
70
  ## Compiled installation
66
71
 
@@ -69,3 +74,5 @@ The registry package contains compiled ESM JavaScript and declarations. Consumer
69
74
  This package is MIT licensed. The license applies to this package and does not license the private Artifacts application repository.
70
75
 
71
76
  Permanent deletion from the panel invalidates historical content-bearing receipts. An exact create, update, publish or move retry for a deleted artifact returns `not_found`; request identity remains reserved, so a changed command still returns `request_conflict` and an old create cannot recreate it. Live-session and scope checks still precede replay. This package exposes no deletion operation.
77
+
78
+ `notify: true` requests one invitation attempt. The result adds `notification` with `status: "requested"`, or `status: "skipped"` and `reason: "disabled"` or `"rate_limited"`. A rate-limited result includes the next UTC day in `retryAt`. Access succeeds in each case. `requested` does not confirm delivery. Exact retries never send again. A fresh request can notify existing access. `requireNew: true` atomically rejects existing access with `conflict` and `details.reason: "share_exists"`. Plain and explicit-false results keep the five grant fields.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-client",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Typed HTTP client for the Artifacts REST API.",
@@ -26,7 +26,7 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@yargote/artifacts-contracts": "0.1.0-beta.5",
29
+ "@yargote/artifacts-contracts": "0.1.0-beta.6",
30
30
  "zod": "4.6.5"
31
31
  },
32
32
  "publishConfig": {
package/src/index.d.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  import { z } from 'zod';
2
- import { createSchema, exportSchema, historySchema, listSchema, moveResultSchema, moveSchema, publishResultSchema, publishSchema, readSchema, type ArtifactRead, type UpdateInput } from '@yargote/artifacts-contracts';
2
+ import { createSchema, exportSchema, historySchema, listSchema, listSharesSchema, moveResultSchema, moveSchema, publishResultSchema, publishSchema, readSchema, shareSchema, unshareSchema, type ArtifactRead, type Share, type ShareResult, type UpdateInput } from '@yargote/artifacts-contracts';
3
3
  import { historyResultSchema, listResultSchema, projectsResultSchema, summaryHistoryResultSchema, type Identity } from '@yargote/artifacts-contracts/http';
4
4
  import type { CompactArtifactRead } from '@yargote/artifacts-contracts/workflow';
5
- export type { ArtifactRead, Snapshot, UpdateInput } from '@yargote/artifacts-contracts';
5
+ export type { ArtifactRead, Share, Snapshot, UpdateInput } from '@yargote/artifacts-contracts';
6
6
  export type { Identity } from '@yargote/artifacts-contracts/http';
7
7
  export type { CompactArtifactRead, RevisionSummary } from '@yargote/artifacts-contracts/workflow';
8
8
  export { prepareArtifactEdit } from '@yargote/artifacts-contracts/workflow';
9
9
  export type CreateInput = z.input<typeof createSchema>;
10
10
  export type ReadInput = z.input<typeof readSchema>;
11
11
  export type PublishInput = z.input<typeof publishSchema>;
12
+ export type ShareInput = z.input<typeof shareSchema>;
13
+ export type UnshareInput = z.input<typeof unshareSchema>;
14
+ export type ListSharesInput = z.input<typeof listSharesSchema>;
15
+ export type ListSharesResult = {
16
+ items: Share[];
17
+ };
12
18
  export type ListInput = z.input<typeof listSchema>;
13
19
  export type MoveInput = z.input<typeof moveSchema>;
14
20
  export type MoveResult = z.infer<typeof moveResultSchema>;
@@ -62,6 +68,9 @@ export interface ArtifactHttpClient {
62
68
  }): Promise<HistoryResult>;
63
69
  history(input: HistoryInput): Promise<HistoryResult | SummaryHistoryResult>;
64
70
  publish(input: PublishInput): Promise<z.infer<typeof publishResultSchema>>;
71
+ share(input: ShareInput): Promise<ShareResult>;
72
+ unshare(input: UnshareInput): Promise<void>;
73
+ listShares(input: ListSharesInput): Promise<ListSharesResult>;
65
74
  move(input: MoveInput): Promise<MoveResult>;
66
75
  export(input: ExportInput): Promise<ArtifactArchive>;
67
76
  }
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { artifactReadSchema, createSchema, exportSchema, historySchema, listSchema, moveResultSchema, moveSchema, publishResultSchema, publishSchema, readSchema, updateSchema } from '@yargote/artifacts-contracts';
2
+ import { artifactReadSchema, createSchema, exportSchema, historySchema, listSchema, listSharesSchema, moveResultSchema, moveSchema, publishResultSchema, publishSchema, readSchema, shareCommandResultSchema, shareResultSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
3
3
  import { compactArtifactReadSchema, historyResultSchema, httpErrorSchema, identitySchema, listResultSchema, projectsResultSchema, restBase, summaryHistoryResultSchema } from '@yargote/artifacts-contracts/http';
4
4
  export { prepareArtifactEdit } from '@yargote/artifacts-contracts/workflow';
5
5
  export class HttpError extends Error {
@@ -16,6 +16,7 @@ export class HttpError extends Error {
16
16
  }
17
17
  const maxJsonBytes = 64 * 1024 * 1024;
18
18
  const maxArchiveBytes = 8 * 1024 * 1024;
19
+ const listSharesResultSchema = z.strictObject({ items: z.array(shareResultSchema) });
19
20
  async function boundedBytes(response, maximum) {
20
21
  const length = response.headers.get('content-length');
21
22
  if (length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
@@ -71,7 +72,7 @@ export function createArtifactHttpClient(options) {
71
72
  const timeoutMs = options.timeoutMs ?? 30_000;
72
73
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
73
74
  throw new TypeError('timeoutMs must be a positive integer no greater than 2147483647');
74
- async function request(path, method, parse, input, maximum = maxJsonBytes) {
75
+ async function request(path, method, expectedStatus, parse, input, maximum = maxJsonBytes) {
75
76
  const token = typeof options.accessToken === 'string' ? options.accessToken : await options.accessToken();
76
77
  if (!token || /[\r\n]/.test(token))
77
78
  throw new TypeError('A nonempty access token is required');
@@ -101,7 +102,6 @@ export function createArtifactHttpClient(options) {
101
102
  const { error } = parseResponse(httpErrorSchema, value, response.status);
102
103
  throw new HttpError(response.status, error.code, error.message, error.details);
103
104
  }
104
- const expectedStatus = method === 'POST' ? 201 : 200;
105
105
  if (response.status !== expectedStatus)
106
106
  throw new HttpError(response.status, 'invalid_response', `The server returned status ${response.status}, expected ${expectedStatus}`);
107
107
  return await parse(response, bytes);
@@ -118,7 +118,7 @@ export function createArtifactHttpClient(options) {
118
118
  }
119
119
  }
120
120
  function json(path, method, schema, input) {
121
- return request(path, method, (response, bytes) => {
121
+ return request(path, method, method === 'POST' ? 201 : 200, (response, bytes) => {
122
122
  if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json')
123
123
  throw new HttpError(response.status, 'invalid_response', 'The server did not return JSON');
124
124
  let value;
@@ -157,13 +157,28 @@ export function createArtifactHttpClient(options) {
157
157
  const input = publishSchema.parse(value);
158
158
  return json(`/artifacts/${encodeURIComponent(input.artifactId)}/publication`, 'PUT', publishResultSchema, input);
159
159
  },
160
+ share(value) {
161
+ const { artifactId, ...input } = shareSchema.parse(value);
162
+ return json(`/artifacts/${encodeURIComponent(artifactId)}/shares`, 'POST', shareCommandResultSchema, input);
163
+ },
164
+ listShares(value) {
165
+ const { artifactId } = listSharesSchema.parse(value);
166
+ return json(`/artifacts/${encodeURIComponent(artifactId)}/shares`, 'GET', listSharesResultSchema);
167
+ },
168
+ unshare(value) {
169
+ const { artifactId, shareId, ...input } = unshareSchema.parse(value);
170
+ return request(`/artifacts/${encodeURIComponent(artifactId)}/shares/${encodeURIComponent(shareId)}`, 'DELETE', 204, (response, bytes) => {
171
+ if (bytes.byteLength !== 0)
172
+ throw new HttpError(response.status, 'invalid_response', 'The server returned a nonempty response');
173
+ }, input);
174
+ },
160
175
  move(value) {
161
176
  const input = moveSchema.parse(value);
162
177
  return json(`/artifacts/${encodeURIComponent(input.artifactId)}/project`, 'PUT', moveResultSchema, input);
163
178
  },
164
179
  export(value) {
165
180
  const input = exportSchema.parse(value);
166
- return request(`/artifacts/${encodeURIComponent(input.artifactId)}/revisions/${encodeURIComponent(input.revisionId)}/export`, 'GET', async (response, bytes) => {
181
+ return request(`/artifacts/${encodeURIComponent(input.artifactId)}/revisions/${encodeURIComponent(input.revisionId)}/export`, 'GET', 200, async (response, bytes) => {
167
182
  const disposition = response.headers.get('content-disposition');
168
183
  const filename = /^attachment; filename="([A-Za-z0-9._-]+\.zip)"$/.exec(disposition ?? '')?.[1];
169
184
  const digest = response.headers.get('x-artifact-sha256');