@kindgi/blob-binding 0.0.0-bootstrap.0 → 0.1.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 +201 -0
- package/README.md +80 -2
- package/dist/index.d.ts +292 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -4
- package/src/index.ts +350 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Kindgi Inc.
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@kindgi/blob-binding`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Storage contract for Kindgi artifacts (blobs). `BlobStorageBinding` is the interface a storage adapter implements and [`@kindgi/api`](../api/) consumes through its `blobStorage` input; it backs both the `/v1/artifacts/*` surface and the S3-compatible `/s3/*` surface. This package contains types only and has no runtime code.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Keep storage out of the API package while giving both wire surfaces one store. Every blob has two addresses kept in a single metadata record: a `blobId` (used by `/v1/artifacts/*`) and a `(bucket, key)` pair (used by `/s3/*`). Uploads through `put` get a synthetic `(bucket, key)` so they are also reachable over S3, and `head(blobId)` and `headByKey(bucket, key)` return the same `BlobMeta` for the same blob. Every method takes `tenantId` explicitly so adapters can partition storage per tenant. Cursors and continuation tokens are opaque and adapter-defined.
|
|
8
|
+
|
|
9
|
+
Contract rules adapters follow:
|
|
10
|
+
|
|
11
|
+
- `BlobMeta.hash` is the lowercase hex SHA-256 of the exact bytes persisted. When `BlobPutInput.expectedHash` is set, a mismatch fails the write with `blob-hash-mismatch`. The one exception is a multipart-assembled object, whose `hash` is the S3 multipart ETag.
|
|
12
|
+
- `delete` and `deleteByKey` are idempotent: `{ deleted: true }` on the first call, `{ deleted: false }` afterwards. Adapters may tombstone or remove data.
|
|
13
|
+
- `listByPrefix` sorts lexicographically by `key`, as S3 does; `putByKey` overwrites an existing key.
|
|
14
|
+
|
|
15
|
+
## Exports
|
|
16
|
+
|
|
17
|
+
- **`BlobStorageBinding`** — the adapter interface, in three groups:
|
|
18
|
+
- **By `blobId`** — `put(tenantId, input)`, `get(tenantId, blobId)` (body as a `ReadableStream`), `head(tenantId, blobId)` (`null` when unknown), `list(tenantId, filter, cursor?, limit?)`, `delete(tenantId, blobId)`.
|
|
19
|
+
- **By `(bucket, key)`** — `putByKey`, `getByKey`, `headByKey`, `deleteByKey`, and `listByPrefix(tenantId, bucket, prefix, continuationToken?, maxKeys?)` (`maxKeys` 1 to 1000, default 1000).
|
|
20
|
+
- **Multipart upload** — `initiateMultipartUpload` (returns `uploadId`), `uploadPart` (returns the part `etag`), `completeMultipartUpload` (parts in ascending `partNumber`), `abortMultipartUpload`, `listParts`.
|
|
21
|
+
- **Inputs**
|
|
22
|
+
- **`BlobPutInput`** — `name`, `contentType`, `bytes` (`ReadableStream<Uint8Array>` or `Uint8Array`), `size?`, `tags?`, `ownerRunId?`, `expectedHash?`.
|
|
23
|
+
- **`MultipartInitiateInput`** — `contentType`, `tags?`, `ownerRunId?`.
|
|
24
|
+
- **`BlobFilter`** — `ownerRunId`, `contentType`, `tags` (all must match), `scope` (a `Scope` from [`@kindgi/platform`](../platform/)), and `inherit` (no effect here: blobs always live at project level).
|
|
25
|
+
- **Results** — **`BlobMeta`** (`blobId`, `tenantId`, `name`, `contentType`, `size`, `hash`, `tags`, `ownerRunId?`, `createdAt`, `bucket?`, `key?`), **`BlobRead`** (`meta` + `stream`), **`BlobListPage`** (`data`, `nextCursor?`), **`BlobDeleteOutcome`** (`deleted`), **`S3ListPage`** (`contents`, `isTruncated`, `nextContinuationToken?`), **`MultipartListPartsPage`** (`parts` with `partNumber`, `etag`, `size`, `lastModified`).
|
|
26
|
+
- **`BlobError`** — union discriminated by `code`: `blob-not-found` (`blobId`), `blob-hash-mismatch` (`expected`, `actual`), `blob-size-mismatch` (`declared`, `actual`), `blob-storage-error` (`cause?`).
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
|
|
33
|
+
import type { BlobMeta, BlobStorageBinding } from '@kindgi/blob-binding';
|
|
34
|
+
import type { RunId, TenantId } from '@kindgi/types';
|
|
35
|
+
|
|
36
|
+
async function storeReport(
|
|
37
|
+
blobs: BlobStorageBinding,
|
|
38
|
+
tenantId: TenantId,
|
|
39
|
+
runId: RunId,
|
|
40
|
+
report: string,
|
|
41
|
+
): Promise<BlobMeta> {
|
|
42
|
+
const bytes = new TextEncoder().encode(report);
|
|
43
|
+
const stored = await blobs.put(tenantId, {
|
|
44
|
+
name: 'q3-report.txt',
|
|
45
|
+
contentType: 'text/plain',
|
|
46
|
+
bytes,
|
|
47
|
+
size: bytes.byteLength,
|
|
48
|
+
tags: { kind: 'report' },
|
|
49
|
+
ownerRunId: runId,
|
|
50
|
+
// Optional integrity check: the binding rejects the write if its own sha256 differs.
|
|
51
|
+
expectedHash: createHash('sha256').update(bytes).digest('hex'),
|
|
52
|
+
});
|
|
53
|
+
if (stored.kind === 'err') throw new Error(`${stored.error.code}: ${stored.error.message}`);
|
|
54
|
+
const meta = stored.value;
|
|
55
|
+
|
|
56
|
+
// The same object is addressable by (bucket, key) on the S3-compatible surface.
|
|
57
|
+
if (meta.bucket !== undefined && meta.key !== undefined) {
|
|
58
|
+
const viaKey = await blobs.headByKey(tenantId, meta.bucket, meta.key);
|
|
59
|
+
console.log(viaKey?.blobId === meta.blobId); // true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Everything this run has produced with the same tag, first page.
|
|
63
|
+
const page = await blobs.list(tenantId, { ownerRunId: runId, tags: { kind: 'report' } }, undefined, 20);
|
|
64
|
+
if (page.kind === 'ok') console.log(page.value.data.map((b) => `${b.name} ${b.size}B`));
|
|
65
|
+
|
|
66
|
+
return meta;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Non-goals
|
|
71
|
+
|
|
72
|
+
- **No storage implementation.** Filesystem, object-store, and other adapters implement `BlobStorageBinding` in their own packages.
|
|
73
|
+
- **No `Content-MD5` verification.** The S3-compatible route verifies it before calling the binding.
|
|
74
|
+
- **No object versioning.** Writing an existing `(bucket, key)` replaces the object (last write wins).
|
|
75
|
+
- **No scope inheritance for blobs.** Blobs are project-level content; `BlobFilter.inherit` exists only to keep one filter shape across scope-aware bindings.
|
|
76
|
+
|
|
77
|
+
## Related
|
|
78
|
+
|
|
79
|
+
- [`@kindgi/api`](../api/) — mounts `/v1/artifacts/*` over a `BlobStorageBinding`, and `/s3/*` when an S3 credential binding is also supplied.
|
|
80
|
+
- [`@kindgi/platform`](../platform/) — `Scope`.
|
|
81
|
+
- [`@kindgi/types`](../types/) — `ArtifactId`, `Cursor`, `RunId`, `TenantId`, `Timestamp`, `Result`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { Scope } from '@kindgi/platform';
|
|
2
|
+
import type { ArtifactId, Cursor, Result, RunId, TenantId, Timestamp } from '@kindgi/types';
|
|
3
|
+
/**
|
|
4
|
+
* Caller-plugged surface for artifact (blob) storage. Same pattern as
|
|
5
|
+
* `AgentRegistryBinding` / `MemoryBinding` / `ReviewerRegistryBinding`
|
|
6
|
+
* — the API package does NOT own storage. Deployments plug in a
|
|
7
|
+
* binding implementation: typically a filesystem-backed one for
|
|
8
|
+
* development and tests, and an object-store-backed one in production.
|
|
9
|
+
*
|
|
10
|
+
* Every method is tenant-scoped: callers pass `tenantId` explicitly so
|
|
11
|
+
* multi-tenant deployments can partition storage without exposing the
|
|
12
|
+
* scoping inside the API package. Cursors are opaque — the binding
|
|
13
|
+
* chooses its encoding.
|
|
14
|
+
*
|
|
15
|
+
* ## Two lookup surfaces, one storage
|
|
16
|
+
*
|
|
17
|
+
* Kindgi artifacts have two wire faces: the bespoke
|
|
18
|
+
* `/v1/artifacts/*` surface identifies blobs by `blobId` (UUID minted
|
|
19
|
+
* server-side), while the S3-compat `/s3/*` surface identifies them by
|
|
20
|
+
* `(bucket, key)` tuple. Adapters MUST unify these — every blob has
|
|
21
|
+
* both a `blobId` AND a `(bucket, key)` address, stored in a single
|
|
22
|
+
* meta record. This binding exposes both lookup shapes; adapters
|
|
23
|
+
* choose their own on-disk / on-storage layout so long as
|
|
24
|
+
* `head(blobId)` and `headByKey(bucket, key)` return the same
|
|
25
|
+
* `BlobMeta` for a given blob.
|
|
26
|
+
*
|
|
27
|
+
* Bespoke uploads via `put(...)` MUST allocate a synthetic
|
|
28
|
+
* `(bucket, key)` — conventionally
|
|
29
|
+
* `bucket: 'artifacts', key: <blobId>`, so bespoke uploads are
|
|
30
|
+
* visible to any S3 credential granted access to the `artifacts`
|
|
31
|
+
* bucket (cross-surface interop).
|
|
32
|
+
*
|
|
33
|
+
* ## Hash contract
|
|
34
|
+
*
|
|
35
|
+
* `BlobMeta.hash` is `sha256`, hex-encoded, lowercase. Bindings MUST
|
|
36
|
+
* compute it at boundary from the exact byte stream persisted. Callers
|
|
37
|
+
* MAY assert an expected hash on `BlobPutInput.expectedHash`; when
|
|
38
|
+
* present, the binding compares against the computed hash and returns
|
|
39
|
+
* `blob-hash-mismatch` on divergence (dedup + integrity check in one).
|
|
40
|
+
*
|
|
41
|
+
* ## Delete semantics
|
|
42
|
+
*
|
|
43
|
+
* `delete` is soft-delete at the binding level — the contract only
|
|
44
|
+
* requires idempotence: a second delete returns
|
|
45
|
+
* `{ deleted: false }` (already gone), a first delete returns
|
|
46
|
+
* `{ deleted: true }`. Implementations MAY keep tombstones with
|
|
47
|
+
* retention; a development implementation may remove the data
|
|
48
|
+
* outright.
|
|
49
|
+
*/
|
|
50
|
+
export interface BlobStorageBinding {
|
|
51
|
+
/**
|
|
52
|
+
* Store bytes for the bespoke `/v1/artifacts` surface. Adapter
|
|
53
|
+
* allocates the `blobId` and a synthetic `(bucket, key)`
|
|
54
|
+
* (conventionally `bucket: 'artifacts', key: <blobId>`) so the object
|
|
55
|
+
* is also reachable via the S3-compat surface. Route validates the
|
|
56
|
+
* multipart body before calling.
|
|
57
|
+
*/
|
|
58
|
+
put(tenantId: TenantId, input: BlobPutInput): Promise<Result<BlobMeta, BlobError>>;
|
|
59
|
+
/**
|
|
60
|
+
* Fetch bytes + metadata for download by `blobId`. Body is a
|
|
61
|
+
* `ReadableStream` so large blobs stream without buffering. Returns
|
|
62
|
+
* `blob-not-found` when the id is unknown to this tenant.
|
|
63
|
+
*/
|
|
64
|
+
get(tenantId: TenantId, blobId: ArtifactId): Promise<Result<BlobRead, BlobError>>;
|
|
65
|
+
/**
|
|
66
|
+
* Metadata-only lookup by `blobId` — no body. Returns `null` when
|
|
67
|
+
* unknown. The route surfaces `null` as `404 blob-not-found`.
|
|
68
|
+
*/
|
|
69
|
+
head(tenantId: TenantId, blobId: ArtifactId): Promise<BlobMeta | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Cursor-paginated metadata list. Filters compose as AND. Sort is
|
|
72
|
+
* binding-defined (for example `createdAt desc, blobId desc`).
|
|
73
|
+
*/
|
|
74
|
+
list(tenantId: TenantId, filter: BlobFilter, cursor?: Cursor, limit?: number): Promise<Result<BlobListPage, BlobError>>;
|
|
75
|
+
/**
|
|
76
|
+
* Idempotent soft-delete by `blobId`. Returns `{ deleted: true }` on
|
|
77
|
+
* the first call, `{ deleted: false }` on subsequent calls (already
|
|
78
|
+
* gone). The route surfaces both as `200` per API convention; only
|
|
79
|
+
* structural failures (permission, backing store error) become
|
|
80
|
+
* non-`200`.
|
|
81
|
+
*/
|
|
82
|
+
delete(tenantId: TenantId, blobId: ArtifactId): Promise<Result<BlobDeleteOutcome, BlobError>>;
|
|
83
|
+
/**
|
|
84
|
+
* Store bytes under an explicit `(bucket, key)` — the S3-compat
|
|
85
|
+
* `PUT /s3/:bucket/*` route. Adapter still allocates a `blobId`
|
|
86
|
+
* internally so the bespoke surface can address the same object.
|
|
87
|
+
* The route verifies the caller's `Content-MD5` (when supplied)
|
|
88
|
+
* BEFORE calling — the binding is not responsible for MD5
|
|
89
|
+
* verification. If the key already exists, the write overwrites
|
|
90
|
+
* silently (S3 semantics without versioning — last write wins).
|
|
91
|
+
*/
|
|
92
|
+
putByKey(tenantId: TenantId, bucket: string, key: string, input: BlobPutInput): Promise<Result<BlobMeta, BlobError>>;
|
|
93
|
+
/**
|
|
94
|
+
* Fetch bytes + metadata by `(bucket, key)`. Same shape as `get`
|
|
95
|
+
* but with the S3-native address. Returns `blob-not-found` when the
|
|
96
|
+
* key is unknown under the bucket.
|
|
97
|
+
*/
|
|
98
|
+
getByKey(tenantId: TenantId, bucket: string, key: string): Promise<Result<BlobRead, BlobError>>;
|
|
99
|
+
/**
|
|
100
|
+
* Metadata-only lookup by `(bucket, key)`. `null` on unknown. The
|
|
101
|
+
* route surfaces `null` as S3 `NoSuchKey` (HTTP 404).
|
|
102
|
+
*/
|
|
103
|
+
headByKey(tenantId: TenantId, bucket: string, key: string): Promise<BlobMeta | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Idempotent soft-delete by `(bucket, key)`. S3 semantics: 204 on
|
|
106
|
+
* success either way — S3 does not distinguish "was there" vs. "was
|
|
107
|
+
* not." `{ deleted: false }` when the key was already gone; `true`
|
|
108
|
+
* otherwise. Route always responds 204 regardless.
|
|
109
|
+
*/
|
|
110
|
+
deleteByKey(tenantId: TenantId, bucket: string, key: string): Promise<Result<BlobDeleteOutcome, BlobError>>;
|
|
111
|
+
/**
|
|
112
|
+
* S3-style prefix + delimiter list under a bucket. `continuationToken`
|
|
113
|
+
* is the opaque S3 pagination marker (adapter-defined encoding);
|
|
114
|
+
* `maxKeys` bounds the page size (1..1000, default 1000). Returns
|
|
115
|
+
* matching blob metadata + `isTruncated` + optional
|
|
116
|
+
* `nextContinuationToken`. Sort order MUST be lexicographic on `key`
|
|
117
|
+
* ascending (S3 spec).
|
|
118
|
+
*/
|
|
119
|
+
listByPrefix(tenantId: TenantId, bucket: string, prefix: string, continuationToken?: string, maxKeys?: number): Promise<Result<S3ListPage, BlobError>>;
|
|
120
|
+
/**
|
|
121
|
+
* Initiate a multipart upload. Returns an opaque `uploadId` that
|
|
122
|
+
* subsequent `uploadPart` / `completeMultipartUpload` /
|
|
123
|
+
* `abortMultipartUpload` calls use. `contentType` + `tags` are
|
|
124
|
+
* pinned at initiate time (S3 semantics — the client sends them on
|
|
125
|
+
* `POST ?uploads` and they apply to the finalized object).
|
|
126
|
+
*/
|
|
127
|
+
initiateMultipartUpload(tenantId: TenantId, bucket: string, key: string, input: MultipartInitiateInput): Promise<Result<{
|
|
128
|
+
uploadId: string;
|
|
129
|
+
}, BlobError>>;
|
|
130
|
+
/**
|
|
131
|
+
* Upload a single part (bytes). Returns the part's ETag
|
|
132
|
+
* (`md5(part_bytes)`, hex, un-quoted — the route wraps in quotes
|
|
133
|
+
* per S3 wire spec). `partNumber` is 1..10,000. When a part number is
|
|
134
|
+
* uploaded again, adapters MAY either replace the earlier part (last
|
|
135
|
+
* write wins, as in S3) or reject the upload.
|
|
136
|
+
*/
|
|
137
|
+
uploadPart(tenantId: TenantId, bucket: string, key: string, uploadId: string, partNumber: number, bytes: Uint8Array): Promise<Result<{
|
|
138
|
+
etag: string;
|
|
139
|
+
}, BlobError>>;
|
|
140
|
+
/**
|
|
141
|
+
* Atomically assemble the declared parts into the final object.
|
|
142
|
+
* `parts` MUST be in ascending `partNumber` order (S3 spec). The
|
|
143
|
+
* adapter verifies each part's declared ETag matches its stored
|
|
144
|
+
* ETag, concatenates the parts in order, writes the final blob
|
|
145
|
+
* (`putByKey`-equivalent), and cleans the staging area. Returns
|
|
146
|
+
* the finalized `BlobMeta`; the S3 wire spec's `ETag` for a
|
|
147
|
+
* multipart object is `md5(concat(md5(part_i)))-<N>` — the adapter
|
|
148
|
+
* writes this into `BlobMeta.hash` too (deviates from the
|
|
149
|
+
* single-shot `sha256` semantic — the S3 spec's ETag is the only
|
|
150
|
+
* stable value clients rely on for multipart).
|
|
151
|
+
*/
|
|
152
|
+
completeMultipartUpload(tenantId: TenantId, bucket: string, key: string, uploadId: string, parts: readonly {
|
|
153
|
+
readonly partNumber: number;
|
|
154
|
+
readonly etag: string;
|
|
155
|
+
}[]): Promise<Result<BlobMeta, BlobError>>;
|
|
156
|
+
/**
|
|
157
|
+
* Discard all staged parts + metadata for an in-progress upload.
|
|
158
|
+
* Returns `blob-not-found` when the `uploadId` is unknown — including
|
|
159
|
+
* one that was already aborted — which the S3-compat route answers
|
|
160
|
+
* with `404 NoSuchUpload`.
|
|
161
|
+
*/
|
|
162
|
+
abortMultipartUpload(tenantId: TenantId, bucket: string, key: string, uploadId: string): Promise<Result<void, BlobError>>;
|
|
163
|
+
/**
|
|
164
|
+
* Read the parts staged so far for a given uploadId. Sorted by
|
|
165
|
+
* `partNumber` ascending. Returns `blob-not-found` when the
|
|
166
|
+
* uploadId is unknown.
|
|
167
|
+
*/
|
|
168
|
+
listParts(tenantId: TenantId, bucket: string, key: string, uploadId: string): Promise<Result<MultipartListPartsPage, BlobError>>;
|
|
169
|
+
}
|
|
170
|
+
export interface MultipartInitiateInput {
|
|
171
|
+
readonly contentType: string;
|
|
172
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
173
|
+
readonly ownerRunId?: RunId;
|
|
174
|
+
}
|
|
175
|
+
export interface MultipartListPartsPage {
|
|
176
|
+
readonly parts: readonly {
|
|
177
|
+
readonly partNumber: number;
|
|
178
|
+
readonly etag: string;
|
|
179
|
+
readonly size: number;
|
|
180
|
+
readonly lastModified: Timestamp;
|
|
181
|
+
}[];
|
|
182
|
+
}
|
|
183
|
+
export interface BlobPutInput {
|
|
184
|
+
readonly name: string;
|
|
185
|
+
readonly contentType: string;
|
|
186
|
+
readonly bytes: ReadableStream<Uint8Array> | Uint8Array;
|
|
187
|
+
/** If known ahead of streaming. Bindings MAY reject when the actual byte count diverges. */
|
|
188
|
+
readonly size?: number;
|
|
189
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
190
|
+
/** Optional back-ref to the run that produced this blob. Filter target on `list`. */
|
|
191
|
+
readonly ownerRunId?: RunId;
|
|
192
|
+
/**
|
|
193
|
+
* Caller-computed hash for dedup + integrity. `sha256`, hex-encoded,
|
|
194
|
+
* lowercase. When set, bindings MUST reject on mismatch with
|
|
195
|
+
* `blob-hash-mismatch`; when omitted, the binding computes and returns
|
|
196
|
+
* the hash in `BlobMeta.hash`.
|
|
197
|
+
*/
|
|
198
|
+
readonly expectedHash?: string;
|
|
199
|
+
}
|
|
200
|
+
export interface BlobMeta {
|
|
201
|
+
readonly blobId: ArtifactId;
|
|
202
|
+
readonly tenantId: TenantId;
|
|
203
|
+
readonly name: string;
|
|
204
|
+
readonly contentType: string;
|
|
205
|
+
readonly size: number;
|
|
206
|
+
/** `sha256`, hex-encoded, lowercase. */
|
|
207
|
+
readonly hash: string;
|
|
208
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
209
|
+
readonly ownerRunId?: RunId;
|
|
210
|
+
readonly createdAt: Timestamp;
|
|
211
|
+
/**
|
|
212
|
+
* S3-compat address. Every persisted blob carries a `(bucket, key)`
|
|
213
|
+
* pair — bespoke uploads default to `bucket: 'artifacts', key:
|
|
214
|
+
* <blobId>` so they're visible via the S3-compat surface too. When
|
|
215
|
+
* absent, the S3-compat surface treats the blob as unaddressable via
|
|
216
|
+
* S3 (still reachable via bespoke `blobId`).
|
|
217
|
+
*/
|
|
218
|
+
readonly bucket?: string;
|
|
219
|
+
readonly key?: string;
|
|
220
|
+
}
|
|
221
|
+
export interface BlobRead {
|
|
222
|
+
readonly meta: BlobMeta;
|
|
223
|
+
readonly stream: ReadableStream<Uint8Array>;
|
|
224
|
+
}
|
|
225
|
+
export interface BlobFilter {
|
|
226
|
+
readonly ownerRunId?: RunId;
|
|
227
|
+
readonly contentType?: string;
|
|
228
|
+
/** Every entry matches as `tags[key] === value`. All must match. */
|
|
229
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
230
|
+
/**
|
|
231
|
+
* Narrow the list to a specific scope. Absent = no scope narrow
|
|
232
|
+
* (return every row in the tenant the caller can see — admin/audit
|
|
233
|
+
* default).
|
|
234
|
+
*
|
|
235
|
+
* Content-scoped semantics (this binding):
|
|
236
|
+
* - `{ kind: 'project', projectId }` — rows in that project.
|
|
237
|
+
* - `{ kind: 'org', orgId }` — rows in every project belonging to
|
|
238
|
+
* that org.
|
|
239
|
+
* - `{ kind: 'tenant', tenantId }` — every row in the tenant.
|
|
240
|
+
*
|
|
241
|
+
* Blobs are content: every blob belongs to a project, so `inherit`
|
|
242
|
+
* has no effect here.
|
|
243
|
+
*/
|
|
244
|
+
readonly scope?: Scope;
|
|
245
|
+
/**
|
|
246
|
+
* `false` = literal-at-this-scope only (admin/audit view).
|
|
247
|
+
* `true` (default) = inheritance walk (user-facing view).
|
|
248
|
+
* No-op for content-scoped bindings (rows only exist at
|
|
249
|
+
* project-level — there is no upward hierarchy to walk). Kept for
|
|
250
|
+
* uniformity: scope-aware bindings share one filter shape across the
|
|
251
|
+
* SDK and OpenAPI schemas.
|
|
252
|
+
*/
|
|
253
|
+
readonly inherit?: boolean;
|
|
254
|
+
}
|
|
255
|
+
export interface BlobListPage {
|
|
256
|
+
readonly data: readonly BlobMeta[];
|
|
257
|
+
readonly nextCursor?: Cursor;
|
|
258
|
+
}
|
|
259
|
+
export interface BlobDeleteOutcome {
|
|
260
|
+
readonly deleted: boolean;
|
|
261
|
+
}
|
|
262
|
+
/** S3 `ListObjectsV2` result shape (normalized). */
|
|
263
|
+
export interface S3ListPage {
|
|
264
|
+
readonly contents: readonly BlobMeta[];
|
|
265
|
+
readonly isTruncated: boolean;
|
|
266
|
+
/** Present only when `isTruncated: true`. */
|
|
267
|
+
readonly nextContinuationToken?: string;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Discriminated error union. `@kindgi/api` maps each `code` to an HTTP
|
|
271
|
+
* status.
|
|
272
|
+
*/
|
|
273
|
+
export type BlobError = {
|
|
274
|
+
readonly code: 'blob-not-found';
|
|
275
|
+
readonly message: string;
|
|
276
|
+
readonly blobId: string;
|
|
277
|
+
} | {
|
|
278
|
+
readonly code: 'blob-hash-mismatch';
|
|
279
|
+
readonly message: string;
|
|
280
|
+
readonly expected: string;
|
|
281
|
+
readonly actual: string;
|
|
282
|
+
} | {
|
|
283
|
+
readonly code: 'blob-size-mismatch';
|
|
284
|
+
readonly message: string;
|
|
285
|
+
readonly declared: number;
|
|
286
|
+
readonly actual: number;
|
|
287
|
+
} | {
|
|
288
|
+
readonly code: 'blob-storage-error';
|
|
289
|
+
readonly message: string;
|
|
290
|
+
readonly cause?: unknown;
|
|
291
|
+
};
|
|
292
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/blob-binding",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Kindgi™ blob storage binding contract. Types only: the BlobStorageBinding interface that storage implementations provide and @kindgi/api consumes — blobId-addressed put / get / head / list / delete, S3-compatible putByKey / getByKey / headByKey / deleteByKey / listByPrefix, and multipart initiate / upload-part / complete / abort / list-parts — its input and result shapes (BlobPutInput, MultipartInitiateInput, BlobFilter, BlobMeta, BlobRead, BlobListPage, BlobDeleteOutcome, S3ListPage, MultipartListPartsPage), and the BlobError union. No runtime code.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kindgi/kindgi-sdk.git",
|
|
9
|
+
"directory": "packages/blob-binding"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/kindgi/kindgi-sdk/tree/main/packages/blob-binding#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kindgi/kindgi-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@kindgi/platform": "0.1.1",
|
|
31
|
+
"@kindgi/types": "0.1.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.10.5",
|
|
35
|
+
"typescript": "^5.7.3"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { Scope } from '@kindgi/platform';
|
|
5
|
+
import type { ArtifactId, Cursor, Result, RunId, TenantId, Timestamp } from '@kindgi/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Caller-plugged surface for artifact (blob) storage. Same pattern as
|
|
9
|
+
* `AgentRegistryBinding` / `MemoryBinding` / `ReviewerRegistryBinding`
|
|
10
|
+
* — the API package does NOT own storage. Deployments plug in a
|
|
11
|
+
* binding implementation: typically a filesystem-backed one for
|
|
12
|
+
* development and tests, and an object-store-backed one in production.
|
|
13
|
+
*
|
|
14
|
+
* Every method is tenant-scoped: callers pass `tenantId` explicitly so
|
|
15
|
+
* multi-tenant deployments can partition storage without exposing the
|
|
16
|
+
* scoping inside the API package. Cursors are opaque — the binding
|
|
17
|
+
* chooses its encoding.
|
|
18
|
+
*
|
|
19
|
+
* ## Two lookup surfaces, one storage
|
|
20
|
+
*
|
|
21
|
+
* Kindgi artifacts have two wire faces: the bespoke
|
|
22
|
+
* `/v1/artifacts/*` surface identifies blobs by `blobId` (UUID minted
|
|
23
|
+
* server-side), while the S3-compat `/s3/*` surface identifies them by
|
|
24
|
+
* `(bucket, key)` tuple. Adapters MUST unify these — every blob has
|
|
25
|
+
* both a `blobId` AND a `(bucket, key)` address, stored in a single
|
|
26
|
+
* meta record. This binding exposes both lookup shapes; adapters
|
|
27
|
+
* choose their own on-disk / on-storage layout so long as
|
|
28
|
+
* `head(blobId)` and `headByKey(bucket, key)` return the same
|
|
29
|
+
* `BlobMeta` for a given blob.
|
|
30
|
+
*
|
|
31
|
+
* Bespoke uploads via `put(...)` MUST allocate a synthetic
|
|
32
|
+
* `(bucket, key)` — conventionally
|
|
33
|
+
* `bucket: 'artifacts', key: <blobId>`, so bespoke uploads are
|
|
34
|
+
* visible to any S3 credential granted access to the `artifacts`
|
|
35
|
+
* bucket (cross-surface interop).
|
|
36
|
+
*
|
|
37
|
+
* ## Hash contract
|
|
38
|
+
*
|
|
39
|
+
* `BlobMeta.hash` is `sha256`, hex-encoded, lowercase. Bindings MUST
|
|
40
|
+
* compute it at boundary from the exact byte stream persisted. Callers
|
|
41
|
+
* MAY assert an expected hash on `BlobPutInput.expectedHash`; when
|
|
42
|
+
* present, the binding compares against the computed hash and returns
|
|
43
|
+
* `blob-hash-mismatch` on divergence (dedup + integrity check in one).
|
|
44
|
+
*
|
|
45
|
+
* ## Delete semantics
|
|
46
|
+
*
|
|
47
|
+
* `delete` is soft-delete at the binding level — the contract only
|
|
48
|
+
* requires idempotence: a second delete returns
|
|
49
|
+
* `{ deleted: false }` (already gone), a first delete returns
|
|
50
|
+
* `{ deleted: true }`. Implementations MAY keep tombstones with
|
|
51
|
+
* retention; a development implementation may remove the data
|
|
52
|
+
* outright.
|
|
53
|
+
*/
|
|
54
|
+
export interface BlobStorageBinding {
|
|
55
|
+
// -------------- bespoke (blobId-addressed) surface --------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Store bytes for the bespoke `/v1/artifacts` surface. Adapter
|
|
59
|
+
* allocates the `blobId` and a synthetic `(bucket, key)`
|
|
60
|
+
* (conventionally `bucket: 'artifacts', key: <blobId>`) so the object
|
|
61
|
+
* is also reachable via the S3-compat surface. Route validates the
|
|
62
|
+
* multipart body before calling.
|
|
63
|
+
*/
|
|
64
|
+
put(tenantId: TenantId, input: BlobPutInput): Promise<Result<BlobMeta, BlobError>>;
|
|
65
|
+
/**
|
|
66
|
+
* Fetch bytes + metadata for download by `blobId`. Body is a
|
|
67
|
+
* `ReadableStream` so large blobs stream without buffering. Returns
|
|
68
|
+
* `blob-not-found` when the id is unknown to this tenant.
|
|
69
|
+
*/
|
|
70
|
+
get(tenantId: TenantId, blobId: ArtifactId): Promise<Result<BlobRead, BlobError>>;
|
|
71
|
+
/**
|
|
72
|
+
* Metadata-only lookup by `blobId` — no body. Returns `null` when
|
|
73
|
+
* unknown. The route surfaces `null` as `404 blob-not-found`.
|
|
74
|
+
*/
|
|
75
|
+
head(tenantId: TenantId, blobId: ArtifactId): Promise<BlobMeta | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Cursor-paginated metadata list. Filters compose as AND. Sort is
|
|
78
|
+
* binding-defined (for example `createdAt desc, blobId desc`).
|
|
79
|
+
*/
|
|
80
|
+
list(
|
|
81
|
+
tenantId: TenantId,
|
|
82
|
+
filter: BlobFilter,
|
|
83
|
+
cursor?: Cursor,
|
|
84
|
+
limit?: number,
|
|
85
|
+
): Promise<Result<BlobListPage, BlobError>>;
|
|
86
|
+
/**
|
|
87
|
+
* Idempotent soft-delete by `blobId`. Returns `{ deleted: true }` on
|
|
88
|
+
* the first call, `{ deleted: false }` on subsequent calls (already
|
|
89
|
+
* gone). The route surfaces both as `200` per API convention; only
|
|
90
|
+
* structural failures (permission, backing store error) become
|
|
91
|
+
* non-`200`.
|
|
92
|
+
*/
|
|
93
|
+
delete(tenantId: TenantId, blobId: ArtifactId): Promise<Result<BlobDeleteOutcome, BlobError>>;
|
|
94
|
+
|
|
95
|
+
// -------------- S3-compat (bucket/key-addressed) surface --------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Store bytes under an explicit `(bucket, key)` — the S3-compat
|
|
99
|
+
* `PUT /s3/:bucket/*` route. Adapter still allocates a `blobId`
|
|
100
|
+
* internally so the bespoke surface can address the same object.
|
|
101
|
+
* The route verifies the caller's `Content-MD5` (when supplied)
|
|
102
|
+
* BEFORE calling — the binding is not responsible for MD5
|
|
103
|
+
* verification. If the key already exists, the write overwrites
|
|
104
|
+
* silently (S3 semantics without versioning — last write wins).
|
|
105
|
+
*/
|
|
106
|
+
putByKey(
|
|
107
|
+
tenantId: TenantId,
|
|
108
|
+
bucket: string,
|
|
109
|
+
key: string,
|
|
110
|
+
input: BlobPutInput,
|
|
111
|
+
): Promise<Result<BlobMeta, BlobError>>;
|
|
112
|
+
/**
|
|
113
|
+
* Fetch bytes + metadata by `(bucket, key)`. Same shape as `get`
|
|
114
|
+
* but with the S3-native address. Returns `blob-not-found` when the
|
|
115
|
+
* key is unknown under the bucket.
|
|
116
|
+
*/
|
|
117
|
+
getByKey(tenantId: TenantId, bucket: string, key: string): Promise<Result<BlobRead, BlobError>>;
|
|
118
|
+
/**
|
|
119
|
+
* Metadata-only lookup by `(bucket, key)`. `null` on unknown. The
|
|
120
|
+
* route surfaces `null` as S3 `NoSuchKey` (HTTP 404).
|
|
121
|
+
*/
|
|
122
|
+
headByKey(tenantId: TenantId, bucket: string, key: string): Promise<BlobMeta | null>;
|
|
123
|
+
/**
|
|
124
|
+
* Idempotent soft-delete by `(bucket, key)`. S3 semantics: 204 on
|
|
125
|
+
* success either way — S3 does not distinguish "was there" vs. "was
|
|
126
|
+
* not." `{ deleted: false }` when the key was already gone; `true`
|
|
127
|
+
* otherwise. Route always responds 204 regardless.
|
|
128
|
+
*/
|
|
129
|
+
deleteByKey(
|
|
130
|
+
tenantId: TenantId,
|
|
131
|
+
bucket: string,
|
|
132
|
+
key: string,
|
|
133
|
+
): Promise<Result<BlobDeleteOutcome, BlobError>>;
|
|
134
|
+
/**
|
|
135
|
+
* S3-style prefix + delimiter list under a bucket. `continuationToken`
|
|
136
|
+
* is the opaque S3 pagination marker (adapter-defined encoding);
|
|
137
|
+
* `maxKeys` bounds the page size (1..1000, default 1000). Returns
|
|
138
|
+
* matching blob metadata + `isTruncated` + optional
|
|
139
|
+
* `nextContinuationToken`. Sort order MUST be lexicographic on `key`
|
|
140
|
+
* ascending (S3 spec).
|
|
141
|
+
*/
|
|
142
|
+
listByPrefix(
|
|
143
|
+
tenantId: TenantId,
|
|
144
|
+
bucket: string,
|
|
145
|
+
prefix: string,
|
|
146
|
+
continuationToken?: string,
|
|
147
|
+
maxKeys?: number,
|
|
148
|
+
): Promise<Result<S3ListPage, BlobError>>;
|
|
149
|
+
|
|
150
|
+
// -------------- S3-compat multipart upload surface --------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Initiate a multipart upload. Returns an opaque `uploadId` that
|
|
154
|
+
* subsequent `uploadPart` / `completeMultipartUpload` /
|
|
155
|
+
* `abortMultipartUpload` calls use. `contentType` + `tags` are
|
|
156
|
+
* pinned at initiate time (S3 semantics — the client sends them on
|
|
157
|
+
* `POST ?uploads` and they apply to the finalized object).
|
|
158
|
+
*/
|
|
159
|
+
initiateMultipartUpload(
|
|
160
|
+
tenantId: TenantId,
|
|
161
|
+
bucket: string,
|
|
162
|
+
key: string,
|
|
163
|
+
input: MultipartInitiateInput,
|
|
164
|
+
): Promise<Result<{ uploadId: string }, BlobError>>;
|
|
165
|
+
/**
|
|
166
|
+
* Upload a single part (bytes). Returns the part's ETag
|
|
167
|
+
* (`md5(part_bytes)`, hex, un-quoted — the route wraps in quotes
|
|
168
|
+
* per S3 wire spec). `partNumber` is 1..10,000. When a part number is
|
|
169
|
+
* uploaded again, adapters MAY either replace the earlier part (last
|
|
170
|
+
* write wins, as in S3) or reject the upload.
|
|
171
|
+
*/
|
|
172
|
+
uploadPart(
|
|
173
|
+
tenantId: TenantId,
|
|
174
|
+
bucket: string,
|
|
175
|
+
key: string,
|
|
176
|
+
uploadId: string,
|
|
177
|
+
partNumber: number,
|
|
178
|
+
bytes: Uint8Array,
|
|
179
|
+
): Promise<Result<{ etag: string }, BlobError>>;
|
|
180
|
+
/**
|
|
181
|
+
* Atomically assemble the declared parts into the final object.
|
|
182
|
+
* `parts` MUST be in ascending `partNumber` order (S3 spec). The
|
|
183
|
+
* adapter verifies each part's declared ETag matches its stored
|
|
184
|
+
* ETag, concatenates the parts in order, writes the final blob
|
|
185
|
+
* (`putByKey`-equivalent), and cleans the staging area. Returns
|
|
186
|
+
* the finalized `BlobMeta`; the S3 wire spec's `ETag` for a
|
|
187
|
+
* multipart object is `md5(concat(md5(part_i)))-<N>` — the adapter
|
|
188
|
+
* writes this into `BlobMeta.hash` too (deviates from the
|
|
189
|
+
* single-shot `sha256` semantic — the S3 spec's ETag is the only
|
|
190
|
+
* stable value clients rely on for multipart).
|
|
191
|
+
*/
|
|
192
|
+
completeMultipartUpload(
|
|
193
|
+
tenantId: TenantId,
|
|
194
|
+
bucket: string,
|
|
195
|
+
key: string,
|
|
196
|
+
uploadId: string,
|
|
197
|
+
parts: readonly { readonly partNumber: number; readonly etag: string }[],
|
|
198
|
+
): Promise<Result<BlobMeta, BlobError>>;
|
|
199
|
+
/**
|
|
200
|
+
* Discard all staged parts + metadata for an in-progress upload.
|
|
201
|
+
* Returns `blob-not-found` when the `uploadId` is unknown — including
|
|
202
|
+
* one that was already aborted — which the S3-compat route answers
|
|
203
|
+
* with `404 NoSuchUpload`.
|
|
204
|
+
*/
|
|
205
|
+
abortMultipartUpload(
|
|
206
|
+
tenantId: TenantId,
|
|
207
|
+
bucket: string,
|
|
208
|
+
key: string,
|
|
209
|
+
uploadId: string,
|
|
210
|
+
): Promise<Result<void, BlobError>>;
|
|
211
|
+
/**
|
|
212
|
+
* Read the parts staged so far for a given uploadId. Sorted by
|
|
213
|
+
* `partNumber` ascending. Returns `blob-not-found` when the
|
|
214
|
+
* uploadId is unknown.
|
|
215
|
+
*/
|
|
216
|
+
listParts(
|
|
217
|
+
tenantId: TenantId,
|
|
218
|
+
bucket: string,
|
|
219
|
+
key: string,
|
|
220
|
+
uploadId: string,
|
|
221
|
+
): Promise<Result<MultipartListPartsPage, BlobError>>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface MultipartInitiateInput {
|
|
225
|
+
readonly contentType: string;
|
|
226
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
227
|
+
readonly ownerRunId?: RunId;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface MultipartListPartsPage {
|
|
231
|
+
readonly parts: readonly {
|
|
232
|
+
readonly partNumber: number;
|
|
233
|
+
readonly etag: string;
|
|
234
|
+
readonly size: number;
|
|
235
|
+
readonly lastModified: Timestamp;
|
|
236
|
+
}[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface BlobPutInput {
|
|
240
|
+
readonly name: string;
|
|
241
|
+
readonly contentType: string;
|
|
242
|
+
readonly bytes: ReadableStream<Uint8Array> | Uint8Array;
|
|
243
|
+
/** If known ahead of streaming. Bindings MAY reject when the actual byte count diverges. */
|
|
244
|
+
readonly size?: number;
|
|
245
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
246
|
+
/** Optional back-ref to the run that produced this blob. Filter target on `list`. */
|
|
247
|
+
readonly ownerRunId?: RunId;
|
|
248
|
+
/**
|
|
249
|
+
* Caller-computed hash for dedup + integrity. `sha256`, hex-encoded,
|
|
250
|
+
* lowercase. When set, bindings MUST reject on mismatch with
|
|
251
|
+
* `blob-hash-mismatch`; when omitted, the binding computes and returns
|
|
252
|
+
* the hash in `BlobMeta.hash`.
|
|
253
|
+
*/
|
|
254
|
+
readonly expectedHash?: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface BlobMeta {
|
|
258
|
+
readonly blobId: ArtifactId;
|
|
259
|
+
readonly tenantId: TenantId;
|
|
260
|
+
readonly name: string;
|
|
261
|
+
readonly contentType: string;
|
|
262
|
+
readonly size: number;
|
|
263
|
+
/** `sha256`, hex-encoded, lowercase. */
|
|
264
|
+
readonly hash: string;
|
|
265
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
266
|
+
readonly ownerRunId?: RunId;
|
|
267
|
+
readonly createdAt: Timestamp;
|
|
268
|
+
/**
|
|
269
|
+
* S3-compat address. Every persisted blob carries a `(bucket, key)`
|
|
270
|
+
* pair — bespoke uploads default to `bucket: 'artifacts', key:
|
|
271
|
+
* <blobId>` so they're visible via the S3-compat surface too. When
|
|
272
|
+
* absent, the S3-compat surface treats the blob as unaddressable via
|
|
273
|
+
* S3 (still reachable via bespoke `blobId`).
|
|
274
|
+
*/
|
|
275
|
+
readonly bucket?: string;
|
|
276
|
+
readonly key?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface BlobRead {
|
|
280
|
+
readonly meta: BlobMeta;
|
|
281
|
+
readonly stream: ReadableStream<Uint8Array>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface BlobFilter {
|
|
285
|
+
readonly ownerRunId?: RunId;
|
|
286
|
+
readonly contentType?: string;
|
|
287
|
+
/** Every entry matches as `tags[key] === value`. All must match. */
|
|
288
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
289
|
+
/**
|
|
290
|
+
* Narrow the list to a specific scope. Absent = no scope narrow
|
|
291
|
+
* (return every row in the tenant the caller can see — admin/audit
|
|
292
|
+
* default).
|
|
293
|
+
*
|
|
294
|
+
* Content-scoped semantics (this binding):
|
|
295
|
+
* - `{ kind: 'project', projectId }` — rows in that project.
|
|
296
|
+
* - `{ kind: 'org', orgId }` — rows in every project belonging to
|
|
297
|
+
* that org.
|
|
298
|
+
* - `{ kind: 'tenant', tenantId }` — every row in the tenant.
|
|
299
|
+
*
|
|
300
|
+
* Blobs are content: every blob belongs to a project, so `inherit`
|
|
301
|
+
* has no effect here.
|
|
302
|
+
*/
|
|
303
|
+
readonly scope?: Scope;
|
|
304
|
+
/**
|
|
305
|
+
* `false` = literal-at-this-scope only (admin/audit view).
|
|
306
|
+
* `true` (default) = inheritance walk (user-facing view).
|
|
307
|
+
* No-op for content-scoped bindings (rows only exist at
|
|
308
|
+
* project-level — there is no upward hierarchy to walk). Kept for
|
|
309
|
+
* uniformity: scope-aware bindings share one filter shape across the
|
|
310
|
+
* SDK and OpenAPI schemas.
|
|
311
|
+
*/
|
|
312
|
+
readonly inherit?: boolean;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface BlobListPage {
|
|
316
|
+
readonly data: readonly BlobMeta[];
|
|
317
|
+
readonly nextCursor?: Cursor;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface BlobDeleteOutcome {
|
|
321
|
+
readonly deleted: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** S3 `ListObjectsV2` result shape (normalized). */
|
|
325
|
+
export interface S3ListPage {
|
|
326
|
+
readonly contents: readonly BlobMeta[];
|
|
327
|
+
readonly isTruncated: boolean;
|
|
328
|
+
/** Present only when `isTruncated: true`. */
|
|
329
|
+
readonly nextContinuationToken?: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Discriminated error union. `@kindgi/api` maps each `code` to an HTTP
|
|
334
|
+
* status.
|
|
335
|
+
*/
|
|
336
|
+
export type BlobError =
|
|
337
|
+
| { readonly code: 'blob-not-found'; readonly message: string; readonly blobId: string }
|
|
338
|
+
| {
|
|
339
|
+
readonly code: 'blob-hash-mismatch';
|
|
340
|
+
readonly message: string;
|
|
341
|
+
readonly expected: string;
|
|
342
|
+
readonly actual: string;
|
|
343
|
+
}
|
|
344
|
+
| {
|
|
345
|
+
readonly code: 'blob-size-mismatch';
|
|
346
|
+
readonly message: string;
|
|
347
|
+
readonly declared: number;
|
|
348
|
+
readonly actual: number;
|
|
349
|
+
}
|
|
350
|
+
| { readonly code: 'blob-storage-error'; readonly message: string; readonly cause?: unknown };
|