@smthrs/artifacts 0.0.0-stage → 1.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +173 -2
  4. package/dist/cjs/ArtifactBackupLease.d.ts +34 -0
  5. package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -0
  6. package/dist/cjs/ArtifactBackupLease.js +175 -0
  7. package/dist/cjs/ArtifactBackupLease.js.map +7 -0
  8. package/dist/cjs/ArtifactStore.d.ts +129 -0
  9. package/dist/cjs/ArtifactStore.d.ts.map +1 -0
  10. package/dist/cjs/ArtifactStore.js +73 -0
  11. package/dist/cjs/ArtifactStore.js.map +7 -0
  12. package/dist/cjs/ArtifactStoreError.d.ts +82 -0
  13. package/dist/cjs/ArtifactStoreError.d.ts.map +1 -0
  14. package/dist/cjs/ArtifactStoreError.js +72 -0
  15. package/dist/cjs/ArtifactStoreError.js.map +7 -0
  16. package/dist/cjs/ArtifactStoreMetrics.d.ts +70 -0
  17. package/dist/cjs/ArtifactStoreMetrics.d.ts.map +1 -0
  18. package/dist/cjs/ArtifactStoreMetrics.js +51 -0
  19. package/dist/cjs/ArtifactStoreMetrics.js.map +7 -0
  20. package/dist/cjs/ArtifactSweep.d.ts +185 -0
  21. package/dist/cjs/ArtifactSweep.d.ts.map +1 -0
  22. package/dist/cjs/ArtifactSweep.js +163 -0
  23. package/dist/cjs/ArtifactSweep.js.map +7 -0
  24. package/dist/cjs/CombinedArtifacts.d.ts +103 -0
  25. package/dist/cjs/CombinedArtifacts.d.ts.map +1 -0
  26. package/dist/cjs/CombinedArtifacts.js +189 -0
  27. package/dist/cjs/CombinedArtifacts.js.map +7 -0
  28. package/dist/cjs/FileLease.d.ts +113 -0
  29. package/dist/cjs/FileLease.d.ts.map +1 -0
  30. package/dist/cjs/FileLease.js +201 -0
  31. package/dist/cjs/FileLease.js.map +7 -0
  32. package/dist/cjs/FileSystemArtifactStore.d.ts +93 -0
  33. package/dist/cjs/FileSystemArtifactStore.d.ts.map +1 -0
  34. package/dist/cjs/FileSystemArtifactStore.js +276 -0
  35. package/dist/cjs/FileSystemArtifactStore.js.map +7 -0
  36. package/dist/cjs/MemoryArtifactStore.d.ts +25 -0
  37. package/dist/cjs/MemoryArtifactStore.d.ts.map +1 -0
  38. package/dist/cjs/MemoryArtifactStore.js +84 -0
  39. package/dist/cjs/MemoryArtifactStore.js.map +7 -0
  40. package/dist/cjs/NoopArtifactStore.d.ts +17 -0
  41. package/dist/cjs/NoopArtifactStore.d.ts.map +1 -0
  42. package/dist/cjs/NoopArtifactStore.js +46 -0
  43. package/dist/cjs/NoopArtifactStore.js.map +7 -0
  44. package/dist/cjs/RemoteArtifacts.d.ts +237 -0
  45. package/dist/cjs/RemoteArtifacts.d.ts.map +1 -0
  46. package/dist/cjs/RemoteArtifacts.js +405 -0
  47. package/dist/cjs/RemoteArtifacts.js.map +7 -0
  48. package/dist/cjs/index.d.ts +65 -0
  49. package/dist/cjs/index.d.ts.map +1 -0
  50. package/dist/cjs/index.js +47 -0
  51. package/dist/cjs/index.js.map +7 -0
  52. package/dist/cjs/internal/ArtifactLocks.d.ts +30 -0
  53. package/dist/cjs/internal/ArtifactLocks.d.ts.map +1 -0
  54. package/dist/cjs/internal/ArtifactLocks.js +93 -0
  55. package/dist/cjs/internal/ArtifactLocks.js.map +7 -0
  56. package/dist/cjs/internal/ArtifactPath.d.ts +19 -0
  57. package/dist/cjs/internal/ArtifactPath.d.ts.map +1 -0
  58. package/dist/cjs/internal/ArtifactPath.js +74 -0
  59. package/dist/cjs/internal/ArtifactPath.js.map +7 -0
  60. package/dist/cjs/measureBytes.d.ts +18 -0
  61. package/dist/cjs/measureBytes.d.ts.map +1 -0
  62. package/dist/cjs/measureBytes.js +43 -0
  63. package/dist/cjs/measureBytes.js.map +7 -0
  64. package/dist/cjs/package.json +1 -0
  65. package/dist/cjs/snapshotBytes.d.ts +19 -0
  66. package/dist/cjs/snapshotBytes.d.ts.map +1 -0
  67. package/dist/cjs/snapshotBytes.js +41 -0
  68. package/dist/cjs/snapshotBytes.js.map +7 -0
  69. package/dist/cjs/validateDigest.d.ts +22 -0
  70. package/dist/cjs/validateDigest.d.ts.map +1 -0
  71. package/dist/cjs/validateDigest.js +42 -0
  72. package/dist/cjs/validateDigest.js.map +7 -0
  73. package/dist/esm/ArtifactBackupLease.d.ts +34 -0
  74. package/dist/esm/ArtifactBackupLease.d.ts.map +1 -0
  75. package/dist/esm/ArtifactBackupLease.js +117 -0
  76. package/dist/esm/ArtifactBackupLease.js.map +1 -0
  77. package/dist/esm/ArtifactStore.d.ts +129 -0
  78. package/dist/esm/ArtifactStore.d.ts.map +1 -0
  79. package/dist/esm/ArtifactStore.js +83 -0
  80. package/dist/esm/ArtifactStore.js.map +1 -0
  81. package/dist/esm/ArtifactStoreError.d.ts +82 -0
  82. package/dist/esm/ArtifactStoreError.d.ts.map +1 -0
  83. package/dist/esm/ArtifactStoreError.js +77 -0
  84. package/dist/esm/ArtifactStoreError.js.map +1 -0
  85. package/dist/esm/ArtifactStoreMetrics.d.ts +70 -0
  86. package/dist/esm/ArtifactStoreMetrics.d.ts.map +1 -0
  87. package/dist/esm/ArtifactStoreMetrics.js +77 -0
  88. package/dist/esm/ArtifactStoreMetrics.js.map +1 -0
  89. package/dist/esm/ArtifactSweep.d.ts +185 -0
  90. package/dist/esm/ArtifactSweep.d.ts.map +1 -0
  91. package/dist/esm/ArtifactSweep.js +202 -0
  92. package/dist/esm/ArtifactSweep.js.map +1 -0
  93. package/dist/esm/CombinedArtifacts.d.ts +103 -0
  94. package/dist/esm/CombinedArtifacts.d.ts.map +1 -0
  95. package/dist/esm/CombinedArtifacts.js +214 -0
  96. package/dist/esm/CombinedArtifacts.js.map +1 -0
  97. package/dist/esm/FileLease.d.ts +113 -0
  98. package/dist/esm/FileLease.d.ts.map +1 -0
  99. package/dist/esm/FileLease.js +239 -0
  100. package/dist/esm/FileLease.js.map +1 -0
  101. package/dist/esm/FileSystemArtifactStore.d.ts +93 -0
  102. package/dist/esm/FileSystemArtifactStore.d.ts.map +1 -0
  103. package/dist/esm/FileSystemArtifactStore.js +336 -0
  104. package/dist/esm/FileSystemArtifactStore.js.map +1 -0
  105. package/dist/esm/MemoryArtifactStore.d.ts +25 -0
  106. package/dist/esm/MemoryArtifactStore.d.ts.map +1 -0
  107. package/dist/esm/MemoryArtifactStore.js +67 -0
  108. package/dist/esm/MemoryArtifactStore.js.map +1 -0
  109. package/dist/esm/NoopArtifactStore.d.ts +17 -0
  110. package/dist/esm/NoopArtifactStore.d.ts.map +1 -0
  111. package/dist/esm/NoopArtifactStore.js +27 -0
  112. package/dist/esm/NoopArtifactStore.js.map +1 -0
  113. package/dist/esm/RemoteArtifacts.d.ts +237 -0
  114. package/dist/esm/RemoteArtifacts.d.ts.map +1 -0
  115. package/dist/esm/RemoteArtifacts.js +554 -0
  116. package/dist/esm/RemoteArtifacts.js.map +1 -0
  117. package/dist/esm/index.d.ts +65 -0
  118. package/dist/esm/index.d.ts.map +1 -0
  119. package/dist/esm/index.js +65 -0
  120. package/dist/esm/index.js.map +1 -0
  121. package/dist/esm/internal/ArtifactLocks.d.ts +30 -0
  122. package/dist/esm/internal/ArtifactLocks.d.ts.map +1 -0
  123. package/dist/esm/internal/ArtifactLocks.js +86 -0
  124. package/dist/esm/internal/ArtifactLocks.js.map +1 -0
  125. package/dist/esm/internal/ArtifactPath.d.ts +19 -0
  126. package/dist/esm/internal/ArtifactPath.d.ts.map +1 -0
  127. package/dist/esm/internal/ArtifactPath.js +59 -0
  128. package/dist/esm/internal/ArtifactPath.js.map +1 -0
  129. package/dist/esm/measureBytes.d.ts +18 -0
  130. package/dist/esm/measureBytes.d.ts.map +1 -0
  131. package/dist/esm/measureBytes.js +22 -0
  132. package/dist/esm/measureBytes.js.map +1 -0
  133. package/dist/esm/snapshotBytes.d.ts +19 -0
  134. package/dist/esm/snapshotBytes.d.ts.map +1 -0
  135. package/dist/esm/snapshotBytes.js +30 -0
  136. package/dist/esm/snapshotBytes.js.map +1 -0
  137. package/dist/esm/validateDigest.d.ts +22 -0
  138. package/dist/esm/validateDigest.d.ts.map +1 -0
  139. package/dist/esm/validateDigest.js +26 -0
  140. package/dist/esm/validateDigest.js.map +1 -0
  141. package/docs/README.md +200 -0
  142. package/docs/api.md +474 -0
  143. package/docs/concepts/content-addressing.md +174 -0
  144. package/docs/concepts/coordination.md +141 -0
  145. package/docs/concepts/tiers.md +147 -0
  146. package/docs/guides/fence-a-backup.md +123 -0
  147. package/docs/guides/reclaim-disk-space.md +143 -0
  148. package/docs/guides/serve-the-artifact-protocol.md +140 -0
  149. package/docs/guides/share-artifacts-across-machines.md +180 -0
  150. package/docs/guides/test-against-an-artifact-store.md +158 -0
  151. package/docs/installation.md +98 -0
  152. package/docs/quickstart.md +182 -0
  153. package/docs/troubleshooting.md +232 -0
  154. package/package.json +179 -3
  155. package/src/ArtifactBackupLease.ts +213 -0
  156. package/src/ArtifactStore.ts +152 -0
  157. package/src/ArtifactStoreError.ts +96 -0
  158. package/src/ArtifactStoreMetrics.ts +83 -0
  159. package/src/ArtifactSweep.ts +336 -0
  160. package/src/CombinedArtifacts.ts +369 -0
  161. package/src/FileLease.ts +380 -0
  162. package/src/FileSystemArtifactStore.ts +459 -0
  163. package/src/MemoryArtifactStore.ts +83 -0
  164. package/src/NoopArtifactStore.ts +30 -0
  165. package/src/RemoteArtifacts.ts +809 -0
  166. package/src/index.ts +71 -0
  167. package/src/internal/ArtifactLocks.ts +110 -0
  168. package/src/internal/ArtifactPath.ts +72 -0
  169. package/src/measureBytes.ts +28 -0
  170. package/src/snapshotBytes.ts +34 -0
  171. package/src/validateDigest.ts +31 -0
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The content-addressed artifact store: bytes addressed by their own digest.
3
+ *
4
+ * `ArtifactStore` is the byte half of the cache. `@smthrs/step-cache` maps a
5
+ * step key to a recorded result, while large result bytes live here under their
6
+ * digest. The package depends on `effect` and `@smthrs/crypto`, owns no SQL,
7
+ * and bundles for the browser: host access is Effect's `FileSystem` and
8
+ * `HttpClient` tags, both of which the capability kernel decorates in place.
9
+ *
10
+ * ```ts
11
+ * import { ArtifactStore, CombinedArtifacts, RemoteArtifacts } from "@smthrs/artifacts"
12
+ * import * as Effect from "effect/Effect"
13
+ * import * as FileSystem from "effect/FileSystem"
14
+ * import * as Path from "effect/Path"
15
+ *
16
+ * const layer = CombinedArtifacts.layer({
17
+ * local: Effect.map(Effect.all([FileSystem.FileSystem, Path.Path]), ([fs, path]) =>
18
+ * ArtifactStore.makeFileSystem(fs, path)),
19
+ * remote: RemoteArtifacts.make({ endpoint: "https://cache.example.com" })
20
+ * })
21
+ * ```
22
+ *
23
+ * @since 1.0.0-rc.0
24
+ */
25
+
26
+ /**
27
+ * @category services
28
+ * @since 1.0.0-rc.0
29
+ * @slop
30
+ */
31
+ export * as ArtifactStore from "./ArtifactStore.ts"
32
+
33
+ /**
34
+ * @category metrics
35
+ * @since 1.0.0-rc.0
36
+ * @slop
37
+ */
38
+ export * as ArtifactStoreMetrics from "./ArtifactStoreMetrics.ts"
39
+
40
+ /**
41
+ * @category services
42
+ * @since 1.0.0-rc.0
43
+ */
44
+ export * as ArtifactBackupLease from "./ArtifactBackupLease.ts"
45
+
46
+ /**
47
+ * @category services
48
+ * @since 1.0.0-rc.0
49
+ * @slop
50
+ */
51
+ export * as ArtifactSweep from "./ArtifactSweep.ts"
52
+
53
+ /**
54
+ * @category services
55
+ * @since 1.0.0-rc.0
56
+ * @slop
57
+ */
58
+ export * as CombinedArtifacts from "./CombinedArtifacts.ts"
59
+
60
+ /**
61
+ * @category coordination
62
+ * @since 1.0.0-rc.1
63
+ */
64
+ export * as FileLease from "./FileLease.ts"
65
+
66
+ /**
67
+ * @category services
68
+ * @since 1.0.0-rc.0
69
+ * @slop
70
+ */
71
+ export * as RemoteArtifacts from "./RemoteArtifacts.ts"
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Per-digest coordination for filesystem publication and removal.
3
+ *
4
+ * The semaphore is the cheap in-process path. A `FileLease` lock file is the
5
+ * actual workspace-wide fence: every process that can mutate the object
6
+ * directory observes it, and a crashed owner is recovered after its heartbeat
7
+ * expires.
8
+ *
9
+ * @since 1.0.0-rc.0
10
+ */
11
+
12
+ import * as Deferred from "effect/Deferred"
13
+ import * as Effect from "effect/Effect"
14
+ import type * as FileSystem from "effect/FileSystem"
15
+ import * as Semaphore from "effect/Semaphore"
16
+ import * as FileLease from "../FileLease.ts"
17
+ import * as ArtifactPath from "./ArtifactPath.ts"
18
+
19
+ interface Entry {
20
+ readonly semaphore: Semaphore.Semaphore
21
+ users: number
22
+ }
23
+
24
+ const locks = new WeakMap<FileSystem.FileSystem, Map<string, Entry>>()
25
+
26
+ /**
27
+ * The subdirectory of the objects directory that holds lock files and their
28
+ * stale-owner tombstones. Named here rather than spelled in each caller so the
29
+ * store's crash-orphan sweep reclaims exactly what this module creates.
30
+ *
31
+ * @category constants
32
+ * @since 1.0.0-rc.0
33
+ */
34
+ export const directoryName = ".locks"
35
+
36
+ /**
37
+ * Coordinates publication, freshening, and sweep deletion for one digest.
38
+ * `process` is the explicit weaker mode for hosts without atomic create.
39
+ *
40
+ * @category combinators
41
+ * @since 1.0.0-rc.0
42
+ */
43
+ export const withDigest = <A, E, R, E2>(
44
+ fs: FileSystem.FileSystem,
45
+ directory: string,
46
+ digest: string,
47
+ effect: Effect.Effect<A, E, R>,
48
+ failure: (cause: unknown) => E2,
49
+ coordination: "required" | "process" = "required"
50
+ ): Effect.Effect<A, E | E2, R> =>
51
+ // Every execution of the returned effect claims its own share of the entry,
52
+ // which is why the whole body is suspended rather than run when `withDigest`
53
+ // is called. A count taken at construction is wrong in both directions: an
54
+ // effect built and discarded pins an entry nothing will ever release, and one
55
+ // built once and run repeatedly — `ArtifactBackupLease` builds its gate once
56
+ // and runs it on every heartbeat — retires the entry underneath a live holder,
57
+ // so the next caller mints a second semaphore for the same digest and the two
58
+ // serialize against nothing.
59
+ Effect.suspend(() => {
60
+ let byKey = locks.get(fs)
61
+ if (byKey === undefined) {
62
+ byKey = new Map()
63
+ locks.set(fs, byKey)
64
+ }
65
+ const key = JSON.stringify([directory, digest])
66
+ let entry = byKey.get(key)
67
+ if (entry === undefined) {
68
+ entry = { semaphore: Semaphore.makeUnsafe(1), users: 0 }
69
+ byKey.set(key, entry)
70
+ }
71
+ entry.users += 1
72
+ const held = entry
73
+ const table = byKey
74
+
75
+ // The deadline starts before the semaphore wait and ends once both locks
76
+ // are held. The protected operation and its release are never timed out.
77
+ const ready = Deferred.makeUnsafe<void>()
78
+ const deadline = Deferred.await(ready).pipe(
79
+ Effect.timeout(FileLease.defaultAcquireWithin),
80
+ Effect.catchTag("TimeoutError", (cause) => Effect.fail(failure(cause))),
81
+ Effect.andThen(Effect.never)
82
+ )
83
+ const coordinated = coordination === "process"
84
+ ? Deferred.succeed(ready, undefined).pipe(Effect.andThen(effect))
85
+ : Effect.gen(function*() {
86
+ // The lock directory is refused as a symlink before and after it is
87
+ // created, and its identity is re-checked before every lock-file
88
+ // mutation, so a swap of `.locks` for a link cannot redirect lock,
89
+ // claim, or tombstone writes and removals elsewhere. The portable
90
+ // filesystem cannot make a check atomic with the mutation after it.
91
+ const lockDirectory = `${directory}/${directoryName}`
92
+ yield* ArtifactPath.guard(fs, lockDirectory).pipe(Effect.mapError(failure))
93
+ yield* fs.makeDirectory(lockDirectory, { recursive: true, mode: 0o700 }).pipe(Effect.mapError(failure))
94
+ const checkLockDirectory = yield* ArtifactPath.guard(fs, lockDirectory).pipe(Effect.mapError(failure))
95
+ return yield* FileLease.hold(fs, `${lockDirectory}/${digest}.lock`, effect, failure, {
96
+ label: "Artifact lock",
97
+ annotations: { digest },
98
+ onAcquired: Deferred.succeed(ready, undefined).pipe(Effect.asVoid),
99
+ guard: checkLockDirectory
100
+ })
101
+ })
102
+
103
+ return held.semaphore.withPermit(coordinated).pipe(
104
+ Effect.raceFirst(deadline),
105
+ Effect.ensuring(Effect.sync(() => {
106
+ held.users -= 1
107
+ if (held.users === 0 && table.get(key) === held) table.delete(key)
108
+ }))
109
+ )
110
+ })
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Path checks for the portable filesystem adapter. These detect symlinks and
3
+ * observed replacements, but cannot make pathname mutations atomic with the
4
+ * check. Effect's FileSystem has no descriptor-relative rename or unlink.
5
+ *
6
+ * @since 1.0.0-rc.0
7
+ */
8
+
9
+ import * as Effect from "effect/Effect"
10
+ import type * as FileSystem from "effect/FileSystem"
11
+ import * as Option from "effect/Option"
12
+ import * as PlatformError from "effect/PlatformError"
13
+
14
+ /**
15
+ * Checks one entry without accepting a symbolic link and returns a check that
16
+ * also refuses a changed identity. Missing entries are allowed for creation.
17
+ *
18
+ * @category utilities
19
+ * @since 1.0.0-rc.0
20
+ */
21
+ export const guard = (
22
+ fs: FileSystem.FileSystem,
23
+ path: string,
24
+ type: "Directory" | "File" = "Directory",
25
+ expected?: FileSystem.File.Info
26
+ ): Effect.Effect<Effect.Effect<void, PlatformError.PlatformError>, PlatformError.PlatformError> => {
27
+ const refused = () =>
28
+ PlatformError.systemError({
29
+ _tag: "PermissionDenied",
30
+ module: "ArtifactPath",
31
+ method: "guard",
32
+ pathOrDescriptor: path,
33
+ description: "artifact path is a symlink, has the wrong type, or was replaced"
34
+ })
35
+ const inspect = Effect.gen(function*() {
36
+ const linked = yield* fs.readLink(path).pipe(
37
+ Effect.as(true),
38
+ Effect.catch((cause) => {
39
+ // Node readlink uses EINVAL for an existing entry that is not a link.
40
+ // Other failures, including an unsupported host capability, fail closed.
41
+ const underlying = cause.cause
42
+ return cause.reason._tag === "NotFound" ||
43
+ (typeof underlying === "object" && underlying !== null && "code" in underlying &&
44
+ underlying.code === "EINVAL")
45
+ ? Effect.succeed(false)
46
+ : Effect.fail(cause)
47
+ })
48
+ )
49
+ if (linked) return yield* Effect.fail(refused())
50
+ const info = yield* fs.stat(path).pipe(
51
+ Effect.map(Option.some),
52
+ Effect.catch((cause) => cause.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(cause))
53
+ )
54
+ if (Option.isSome(info) && info.value.type !== type) return yield* Effect.fail(refused())
55
+ return info
56
+ })
57
+ const unchanged = (before: Option.Option<FileSystem.File.Info>, after: Option.Option<FileSystem.File.Info>) => {
58
+ if (Option.isNone(before)) return Effect.void
59
+ if (
60
+ Option.isNone(after) || before.value.dev !== after.value.dev ||
61
+ Option.getOrUndefined(before.value.ino) !== Option.getOrUndefined(after.value.ino)
62
+ ) {
63
+ return Effect.fail(refused())
64
+ }
65
+ return Effect.void
66
+ }
67
+ return Effect.gen(function*() {
68
+ const before = yield* inspect
69
+ yield* unchanged(Option.fromUndefinedOr(expected), before)
70
+ return Effect.flatMap(inspect, (after) => unchanged(before, after))
71
+ })
72
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Measuring artifact bytes: the SHA-256 content address of one immutable
3
+ * snapshot, computed through the injected `Crypto` service.
4
+ *
5
+ * @since 1.0.0-rc.0
6
+ */
7
+
8
+ import { Sha256 } from "@smthrs/crypto"
9
+ import type * as Crypto from "effect/Crypto"
10
+ import * as Effect from "effect/Effect"
11
+ import type { Digest } from "./ArtifactStore.ts"
12
+ import { ArtifactStoreError } from "./ArtifactStoreError.ts"
13
+
14
+ const digestFailure = (cause: unknown): ArtifactStoreError =>
15
+ new ArtifactStoreError({
16
+ code: "digest_failed",
17
+ message: "the Crypto service failed to compute an artifact digest",
18
+ cause
19
+ })
20
+
21
+ /**
22
+ * Measures one immutable byte snapshot without ever retaining it in an error.
23
+ *
24
+ * @category utilities
25
+ * @since 1.0.0-rc.0
26
+ */
27
+ export const measureBytes = (bytes: Uint8Array): Effect.Effect<Digest, ArtifactStoreError, Crypto.Crypto> =>
28
+ Sha256.digest(bytes).pipe(Effect.mapError(digestFailure))
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Snapshotting caller-owned artifact bytes, so a caller that reuses its buffer
3
+ * cannot change what is measured or stored.
4
+ *
5
+ * @since 1.0.0-rc.0
6
+ */
7
+
8
+ import * as Effect from "effect/Effect"
9
+ import { ArtifactStoreError } from "./ArtifactStoreError.ts"
10
+
11
+ /**
12
+ * A refused byte copy is the host declining an allocation, not a crypto
13
+ * failure: the Crypto service has not been consulted when this fires, so
14
+ * reporting `digest_failed` would point an operator diagnosing memory pressure
15
+ * at the wrong subsystem. The message is constant, and the buffer is never
16
+ * attached.
17
+ */
18
+ const snapshotFailure = (cause: unknown): ArtifactStoreError =>
19
+ new ArtifactStoreError({ code: "unavailable", message: "the host could not copy the artifact bytes", cause })
20
+
21
+ /**
22
+ * Copies a caller-owned buffer when the returned Effect begins.
23
+ *
24
+ * A host that refuses the copy — a detached buffer, an allocation past what the
25
+ * runtime will give — fails as `unavailable`, the code for a host refusal.
26
+ *
27
+ * @category utilities
28
+ * @since 1.0.0-rc.0
29
+ */
30
+ export const snapshotBytes = (bytes: Uint8Array): Effect.Effect<Uint8Array, ArtifactStoreError> =>
31
+ Effect.try({
32
+ try: () => new Uint8Array(bytes),
33
+ catch: (cause) => snapshotFailure(cause)
34
+ })
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Validating a content address before any implementation logs it or
3
+ * interpolates it into a path or URL.
4
+ *
5
+ * @since 1.0.0-rc.0
6
+ */
7
+
8
+ import * as Effect from "effect/Effect"
9
+ import type { Digest } from "./ArtifactStore.ts"
10
+ import { ArtifactStoreError } from "./ArtifactStoreError.ts"
11
+
12
+ /**
13
+ * Refuses anything other than the canonical SHA-256 address representation.
14
+ *
15
+ * Every implementation validates before logging or interpolating an untrusted
16
+ * value into a path or URL. The failure text is constant and bounded, so even a
17
+ * hostile multi-megabyte value cannot be copied into logs or durable errors.
18
+ *
19
+ * @category predicates
20
+ * @since 1.0.0-rc.0
21
+ * @slop
22
+ */
23
+ export const validateDigest = (digest: string): Effect.Effect<Digest, ArtifactStoreError> =>
24
+ typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest)
25
+ ? Effect.succeed(digest as Digest)
26
+ : Effect.fail(
27
+ new ArtifactStoreError({
28
+ code: "invalid_digest",
29
+ message: "artifact digest must be exactly 64 lowercase hexadecimal characters"
30
+ })
31
+ )