@sealant/api-contracts-next 0.40.0-next.777 → 0.40.0-next.783

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.
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Resource instances: long-running containers a resource domain (a project) runs, such as a
3
+ * Postgres server, a Redis or a mail catcher, with named volumes that outlive every stop.
4
+ *
5
+ * An instance is addressed by its domain and its `key`, the application's own name for it (Mend:
6
+ * `<definition>` or `<definition>@<worktree>`). Its `alias` is the short name a linked workspace
7
+ * resolves it by (`db`). An instance whose `worktreeKey` is null is reached by every workspace
8
+ * linked to the domain; one with a worktree key only by workspaces linked with that key.
9
+ *
10
+ * The spec has no field for privileged mode, capabilities, host networking, host paths, devices,
11
+ * the Docker socket or shared namespaces, and the decoder names each one it is sent.
12
+ */
13
+ import { Result, Schema } from "effect";
14
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
15
+ import { describeRequestIssue } from "./request-refusal.js";
16
+ import { ResourceDomainForbiddenError, ResourceDomainId, ResourceDomainInternalServerError, ResourceDomainNotFoundError, } from "./resource-domains.js";
17
+ export const RESOURCE_INSTANCE_ID_PREFIX = "rin:";
18
+ const RESOURCE_INSTANCE_ID_PATTERN = new RegExp(`^${RESOURCE_INSTANCE_ID_PREFIX}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`);
19
+ const matching = (pattern, message) => Schema.makeFilter((value) => (pattern.test(value) ? undefined : message));
20
+ export const ResourceInstanceId = Schema.String.check(matching(RESOURCE_INSTANCE_ID_PATTERN, "must be a resource instance id (rin:<uuid>)")).pipe(Schema.brand("ResourceInstanceId"));
21
+ export const ResourceInstanceKey = Schema.String.check(matching(/^[a-z0-9][a-z0-9._@-]{0,127}$/, "must be 1 to 128 lower-case letters, digits, '.', '_', '@' or '-', starting with a letter or digit")).pipe(Schema.brand("ResourceInstanceKey"));
22
+ /** One DNS label: the name a linked workspace resolves the instance by. */
23
+ export const ResourceInstanceAlias = Schema.String.check(matching(/^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/, "must be a DNS label: lower-case letters, digits and '-', starting with a letter")).pipe(Schema.brand("ResourceInstanceAlias"));
24
+ /** The application's opaque name for a worktree: links and worktree instances match on it. */
25
+ export const ResourceWorktreeKey = Schema.String.check(matching(/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/, "must be 1 to 128 letters, digits, '.', '_', ':', '@' or '-', starting with a letter or digit")).pipe(Schema.brand("ResourceWorktreeKey"));
26
+ /** An image pinned by digest (a tag may precede it). Core never resolves a tag. */
27
+ export const ResourcePinnedImage = Schema.String.check(matching(/^[a-z0-9][a-z0-9._/:-]*@sha256:[0-9a-f]{64}$/, "must be an image pinned by digest (<repository>[:tag]@sha256:<64 hex>)")).pipe(Schema.brand("ResourcePinnedImage"));
28
+ const VolumeName = Schema.String.check(matching(/^[a-z][a-z0-9-]{0,30}$/, "must be 1 to 31 lower-case letters, digits or '-'"));
29
+ const isMountPath = (path) => {
30
+ if (!path.startsWith("/") || path === "/")
31
+ return "must be an absolute path other than '/'";
32
+ const segments = path.slice(1).split("/");
33
+ if (segments.some((segment) => segment === "." || segment === ".." || !/^[A-Za-z0-9._@+-]+$/.test(segment))) {
34
+ return "must be a normalized absolute path";
35
+ }
36
+ if (["/proc", "/sys", "/dev"].some((root) => path === root || path.startsWith(`${root}/`)))
37
+ return "must not be under /proc, /sys or /dev";
38
+ return undefined;
39
+ };
40
+ const MountPath = Schema.String.check(Schema.makeFilter(isMountPath));
41
+ const EnvName = Schema.String.check(matching(/^[A-Za-z_][A-Za-z0-9_]{0,127}$/, "must be an environment variable name"));
42
+ const withoutNul = Schema.makeFilter((value) => value.includes("\u0000") ? "must not contain a NUL byte" : undefined);
43
+ const EnvValue = Schema.String.check(withoutNul, Schema.isMaxLength(32_768));
44
+ const Argument = Schema.String.check(withoutNul);
45
+ const Port = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }));
46
+ export const resourceInstanceVolumeSchema = Schema.Struct({
47
+ name: VolumeName,
48
+ mountPath: MountPath,
49
+ });
50
+ export const resourceInstanceReadySchema = Schema.Union([
51
+ Schema.Struct({ kind: Schema.Literal("tcp"), port: Port }),
52
+ Schema.Struct({
53
+ kind: Schema.Literal("exec"),
54
+ argv: Schema.Array(Argument).check(Schema.isMinLength(1)),
55
+ user: Schema.NullOr(Schema.String.check(matching(/^[A-Za-z0-9._-]{1,64}$/, "must be a user"))),
56
+ }),
57
+ ]);
58
+ /** Ten days: the longest an instance may wait unreached before Core stops it. */
59
+ export const RESOURCE_INSTANCE_MAX_IDLE_SECONDS = 864_000;
60
+ const distinct = (by, message) => (items) => new Set(items.map(by)).size === items.length ? undefined : message;
61
+ /** What an instance runs. Excess keys are refused; see the header for what has no field. */
62
+ export const resourceInstanceSpecSchema = Schema.Struct({
63
+ alias: ResourceInstanceAlias,
64
+ /** Null: every workspace linked to the domain reaches it. Set: only links with this key. */
65
+ worktreeKey: Schema.NullOr(ResourceWorktreeKey),
66
+ image: ResourcePinnedImage,
67
+ /** Replaces the image's CMD; its ENTRYPOINT always runs. */
68
+ command: Schema.NullOr(Schema.Array(Argument)),
69
+ env: Schema.Record(EnvName, EnvValue),
70
+ /** Named volumes only. Every image `VOLUME` this does not cover gets a named volume too. */
71
+ volumes: Schema.Array(resourceInstanceVolumeSchema).check(Schema.makeFilter(distinct((volume) => volume.name, "volume names must be distinct")), Schema.makeFilter(distinct((volume) => volume.mountPath, "mount paths must be distinct")), Schema.isMaxLength(16)),
72
+ limits: Schema.Struct({
73
+ cpuMillicores: Schema.Int.check(Schema.isBetween({ minimum: 10, maximum: 256_000 })),
74
+ memoryBytes: Schema.Int.check(Schema.isBetween({ minimum: 16 * 1024 * 1024, maximum: Number.MAX_SAFE_INTEGER })),
75
+ }),
76
+ ready: resourceInstanceReadySchema,
77
+ /** Seconds with no live linked executor reaching it before Core stops it. */
78
+ idleStopAfterSeconds: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: RESOURCE_INSTANCE_MAX_IDLE_SECONDS })),
79
+ });
80
+ export const resourceInstanceStopReasonSchema = Schema.Union([
81
+ Schema.Struct({ kind: Schema.Literal("idle") }),
82
+ /** `by` is the application's words for who stopped it (Mend: a display name). */
83
+ Schema.Struct({
84
+ kind: Schema.Literal("person"),
85
+ by: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200)),
86
+ }),
87
+ ]);
88
+ /** What Core last observed of an instance, apart from what was asked of it. Times are ISO. */
89
+ export const resourceInstanceStateSchema = Schema.Union([
90
+ Schema.Struct({ kind: Schema.Literal("not-started") }),
91
+ Schema.Struct({ kind: Schema.Literal("starting"), since: Schema.String }),
92
+ /** Its ready check answered. */
93
+ Schema.Struct({ kind: Schema.Literal("running"), since: Schema.String }),
94
+ Schema.Struct({
95
+ kind: Schema.Literal("stopped"),
96
+ at: Schema.String,
97
+ reason: resourceInstanceStopReasonSchema,
98
+ }),
99
+ /** The container ended on its own; Core starts it again. */
100
+ Schema.Struct({
101
+ kind: Schema.Literal("exited"),
102
+ at: Schema.String,
103
+ exitCode: Schema.NullOr(Schema.Int),
104
+ }),
105
+ /** A start that could not finish: an image pull, a ready check, a volume. */
106
+ Schema.Struct({ kind: Schema.Literal("failed"), at: Schema.String, message: Schema.String }),
107
+ Schema.Struct({ kind: Schema.Literal("archived"), at: Schema.String }),
108
+ ]);
109
+ const NAMESPACE = "an instance shares no namespace with the host or another container";
110
+ const MOUNTS = "an instance mounts named volumes only, never a host path or the Docker socket";
111
+ const CAPABILITIES = "Sealant adds no capability to an instance";
112
+ const NETWORK = "an instance joins only the networks Sealant gives it, never the host's";
113
+ /**
114
+ * What a spec may never ask for, by the names a caller might send it under, each with the sentence
115
+ * the refusal carries. None of them is a field of `resourceInstanceSpecSchema`.
116
+ */
117
+ export const FORBIDDEN_RESOURCE_INSTANCE_SPEC_FIELDS = {
118
+ privileged: "Sealant never runs an instance privileged",
119
+ capAdd: CAPABILITIES,
120
+ cap_add: CAPABILITIES,
121
+ capabilities: CAPABILITIES,
122
+ network: NETWORK,
123
+ networkMode: NETWORK,
124
+ network_mode: NETWORK,
125
+ hostNetwork: NETWORK,
126
+ binds: MOUNTS,
127
+ bind: MOUNTS,
128
+ mounts: MOUNTS,
129
+ hostPath: MOUNTS,
130
+ dockerSocket: MOUNTS,
131
+ devices: "an instance gets no device",
132
+ pid: NAMESPACE,
133
+ ipc: NAMESPACE,
134
+ uts: NAMESPACE,
135
+ userns: NAMESPACE,
136
+ usernsMode: NAMESPACE,
137
+ securityOpt: "an instance's security options are Sealant's",
138
+ security_opt: "an instance's security options are Sealant's",
139
+ };
140
+ const decodeSpec = Schema.decodeUnknownResult(resourceInstanceSpecSchema);
141
+ /**
142
+ * A spec from outside: each forbidden field refused by name with its sentence, then the schema's
143
+ * own decode with no excess key allowed.
144
+ */
145
+ export const decodeResourceInstanceSpec = (input) => {
146
+ const forbidden = typeof input === "object" && input !== null
147
+ ? Object.keys(input).flatMap((field) => Object.hasOwn(FORBIDDEN_RESOURCE_INSTANCE_SPEC_FIELDS, field)
148
+ ? [{ field, reason: FORBIDDEN_RESOURCE_INSTANCE_SPEC_FIELDS[field] ?? "" }]
149
+ : [])
150
+ : [];
151
+ if (forbidden.length > 0)
152
+ return { kind: "refused", refusals: forbidden };
153
+ const decoded = decodeSpec(input, { onExcessProperty: "error", errors: "all" });
154
+ return Result.isSuccess(decoded)
155
+ ? { kind: "spec", spec: decoded.success }
156
+ : {
157
+ kind: "refused",
158
+ refusals: [{ field: "spec", reason: describeRequestIssue("spec", decoded.failure.issue) }],
159
+ };
160
+ };
161
+ // ---------------------------------------------------------------------------------------------
162
+ // Routes, under `/v1/resource-domains/:resourceDomainId`. Only a service key reaches them, as with
163
+ // every resource domain route. `exec` is a raw route beside these (see `RESOURCE_INSTANCE_EXEC`).
164
+ // ---------------------------------------------------------------------------------------------
165
+ export const resourceInstanceUpCauseSchema = Schema.Literals(["use", "person"]);
166
+ export const upResourceInstanceRequestSchema = Schema.Struct({
167
+ /**
168
+ * A `ResourceInstanceSpec`, decoded by `decodeResourceInstanceSpec` so a refused field is named
169
+ * with its sentence (`ResourceInstanceSpecRefusedError`).
170
+ */
171
+ spec: Schema.Record(Schema.String, Schema.Unknown),
172
+ /** `use`: a launch or a wake, which leaves a person's stop standing. `person`: a person's Start. */
173
+ cause: resourceInstanceUpCauseSchema,
174
+ });
175
+ export const stopResourceInstanceRequestSchema = Schema.Struct({
176
+ /** The application's words for who stopped it (Mend: a display name). */
177
+ by: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200)),
178
+ });
179
+ const Timestamp = Schema.String;
180
+ export const resourceInstanceSchema = Schema.Struct({
181
+ resourceInstanceId: ResourceInstanceId,
182
+ key: ResourceInstanceKey,
183
+ alias: ResourceInstanceAlias,
184
+ worktreeKey: Schema.NullOr(ResourceWorktreeKey),
185
+ specDigest: Schema.String,
186
+ /** What was asked of it. */
187
+ desired: Schema.Literals(["running", "stopped", "archived"]),
188
+ /** Why it is stopped, while `desired` is `stopped`. */
189
+ stopReason: Schema.NullOr(resourceInstanceStopReasonSchema),
190
+ /** What Core last observed, and when. */
191
+ state: resourceInstanceStateSchema,
192
+ observedAt: Timestamp,
193
+ startedAt: Schema.NullOr(Timestamp),
194
+ lastReachedAt: Schema.NullOr(Timestamp),
195
+ /** The Docker volumes it mounts, which outlive it. */
196
+ volumes: Schema.Array(Schema.Struct({ name: Schema.String, dockerName: Schema.String })),
197
+ });
198
+ export const resourceInstanceArchivedVolumeSchema = Schema.Struct({
199
+ instanceKey: ResourceInstanceKey,
200
+ name: Schema.String,
201
+ dockerName: Schema.String,
202
+ /** Measured at archive; null when it could not be. */
203
+ bytes: Schema.NullOr(Schema.Int),
204
+ });
205
+ export const listResourceInstancesQuerySchema = Schema.Struct({
206
+ /** `true`: the archived instances, whose volumes are kept. Otherwise every other one. */
207
+ archived: Schema.optional(Schema.Literals(["true", "false"])),
208
+ });
209
+ export const linkResourceWorkspaceRequestSchema = Schema.Struct({
210
+ worktreeKey: ResourceWorktreeKey,
211
+ });
212
+ export const resourceWorkspaceLinkSchema = Schema.Struct({
213
+ workspaceId: Schema.String,
214
+ worktreeKey: ResourceWorktreeKey,
215
+ /** How many of the workspace's live executors were attached by this call. */
216
+ attachedExecutors: Schema.Int,
217
+ });
218
+ /** The spec asks for what an instance may never have, or is not a spec; nothing was written. */
219
+ export class ResourceInstanceSpecRefusedError extends Schema.TaggedError()("ResourceInstanceSpecRefusedError", {
220
+ message: Schema.String,
221
+ refusals: Schema.Array(Schema.Struct({ field: Schema.String, reason: Schema.String })),
222
+ }, { httpApiStatus: 400 }) {
223
+ }
224
+ export class ResourceInstanceNotFoundError extends Schema.TaggedError()("ResourceInstanceNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
225
+ }
226
+ /** This Sealant's workspace runtime cannot run instances: `resources run on Docker; this Sealant runs <runtime>`. */
227
+ export class ResourceRuntimeUnsupportedError extends Schema.TaggedError()("ResourceRuntimeUnsupportedError", { message: Schema.String, runtime: Schema.String }, { httpApiStatus: 409 }) {
228
+ }
229
+ /** The key names an archived instance: it stays archived, and its volumes are kept. */
230
+ export class ResourceInstanceArchivedError extends Schema.TaggedError()("ResourceInstanceArchivedError", { message: Schema.String }, { httpApiStatus: 409 }) {
231
+ }
232
+ /** Another instance that is not archived answers to this alias for the same worktree key. */
233
+ export class ResourceInstanceAliasTakenError extends Schema.TaggedError()("ResourceInstanceAliasTakenError", { message: Schema.String }, { httpApiStatus: 409 }) {
234
+ }
235
+ /** The domain is fenced or archived and admits no new instance or link. */
236
+ export class ResourceDomainNotActiveError extends Schema.TaggedError()("ResourceDomainNotActiveError", { message: Schema.String, state: Schema.Literals(["fenced", "archived"]) }, { httpApiStatus: 409 }) {
237
+ }
238
+ export class ResourceWorkspaceNotFoundError extends Schema.TaggedError()("ResourceWorkspaceNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
239
+ }
240
+ /** A workspace links to one domain; it is linked to `resourceDomainId`. */
241
+ export class ResourceWorkspaceLinkedElsewhereError extends Schema.TaggedError()("ResourceWorkspaceLinkedElsewhereError", { message: Schema.String, resourceDomainId: Schema.String }, { httpApiStatus: 409 }) {
242
+ }
243
+ const instanceParams = Schema.Struct({
244
+ resourceDomainId: ResourceDomainId,
245
+ key: ResourceInstanceKey,
246
+ });
247
+ const domainParams = Schema.Struct({ resourceDomainId: ResourceDomainId });
248
+ const baseErrors = [
249
+ ResourceDomainForbiddenError,
250
+ ResourceDomainNotFoundError,
251
+ ResourceDomainInternalServerError,
252
+ ];
253
+ export const ResourceInstancesGroup = HttpApiGroup.make("resourceInstances")
254
+ .add(HttpApiEndpoint.post("upResourceInstance", "/:resourceDomainId/instances/:key/up", {
255
+ params: instanceParams,
256
+ payload: upResourceInstanceRequestSchema,
257
+ success: resourceInstanceSchema,
258
+ error: [
259
+ ...baseErrors,
260
+ ResourceInstanceSpecRefusedError,
261
+ ResourceRuntimeUnsupportedError,
262
+ ResourceInstanceArchivedError,
263
+ ResourceInstanceAliasTakenError,
264
+ ResourceDomainNotActiveError,
265
+ ],
266
+ }))
267
+ .add(HttpApiEndpoint.get("listResourceInstances", "/:resourceDomainId/instances", {
268
+ params: domainParams,
269
+ query: listResourceInstancesQuerySchema,
270
+ success: Schema.Struct({ items: Schema.Array(resourceInstanceSchema) }),
271
+ error: [...baseErrors],
272
+ }))
273
+ .add(HttpApiEndpoint.get("getResourceInstance", "/:resourceDomainId/instances/:key", {
274
+ params: instanceParams,
275
+ success: resourceInstanceSchema,
276
+ error: [...baseErrors, ResourceInstanceNotFoundError],
277
+ }))
278
+ .add(HttpApiEndpoint.post("stopResourceInstance", "/:resourceDomainId/instances/:key/stop", {
279
+ params: instanceParams,
280
+ payload: stopResourceInstanceRequestSchema,
281
+ success: resourceInstanceSchema,
282
+ error: [
283
+ ...baseErrors,
284
+ ResourceInstanceNotFoundError,
285
+ ResourceInstanceArchivedError,
286
+ ResourceRuntimeUnsupportedError,
287
+ ],
288
+ }))
289
+ .add(HttpApiEndpoint.post("archiveResourceInstance", "/:resourceDomainId/instances/:key/archive", {
290
+ params: instanceParams,
291
+ success: Schema.Struct({
292
+ instance: resourceInstanceSchema,
293
+ volumes: Schema.Array(resourceInstanceArchivedVolumeSchema),
294
+ }),
295
+ error: [...baseErrors, ResourceInstanceNotFoundError, ResourceRuntimeUnsupportedError],
296
+ }))
297
+ .add(HttpApiEndpoint.put("linkResourceWorkspace", "/:resourceDomainId/workspace-links/:workspaceId", {
298
+ params: Schema.Struct({
299
+ resourceDomainId: ResourceDomainId,
300
+ workspaceId: Schema.String.check(Schema.isMinLength(1)),
301
+ }),
302
+ payload: linkResourceWorkspaceRequestSchema,
303
+ success: resourceWorkspaceLinkSchema,
304
+ error: [
305
+ ...baseErrors,
306
+ ResourceWorkspaceNotFoundError,
307
+ ResourceWorkspaceLinkedElsewhereError,
308
+ ResourceDomainNotActiveError,
309
+ ResourceRuntimeUnsupportedError,
310
+ ],
311
+ }))
312
+ .add(HttpApiEndpoint.post("archiveResourceDomain", "/:resourceDomainId/archive", {
313
+ params: domainParams,
314
+ success: Schema.Struct({ volumes: Schema.Array(resourceInstanceArchivedVolumeSchema) }),
315
+ error: [...baseErrors, ResourceRuntimeUnsupportedError],
316
+ }))
317
+ .annotate(HttpApi.PayloadParseOptions, { onExcessProperty: "error" })
318
+ .annotate(OpenApi.Description, "Long-running instances a resource domain runs, and the workspaces that reach them.");
319
+ /**
320
+ * The raw exec route, beside the contract: `POST /v1/resource-domains/:id/instances/:key/exec`.
321
+ * Its body is one JSON line (`execFrame`, at most `maxFrameBytes`) and then the command's stdin,
322
+ * raw, to the end of the body. It answers `ResourceInstanceExecResult` as JSON. No header, query or
323
+ * log line carries the argv or stdin, and no run record is written: a secret goes in stdin.
324
+ */
325
+ export const RESOURCE_INSTANCE_EXEC = {
326
+ path: "/v1/resource-domains/:resourceDomainId/instances/:key/exec",
327
+ maxFrameBytes: 64 * 1024,
328
+ defaultTimeoutMs: 10 * 60_000,
329
+ maxTimeoutMs: 6 * 60 * 60_000,
330
+ };
331
+ export const resourceInstanceExecFrameSchema = Schema.Struct({
332
+ argv: Schema.Array(Argument).check(Schema.isMinLength(1), Schema.isMaxLength(4096)),
333
+ user: Schema.NullOr(Schema.String.check(matching(/^[A-Za-z0-9._:-]{1,64}$/, "must be a user"))),
334
+ timeoutMs: Schema.Int.check(Schema.isBetween({ minimum: 1000, maximum: RESOURCE_INSTANCE_EXEC.maxTimeoutMs })),
335
+ });
336
+ export const resourceInstanceExecResultSchema = Schema.Struct({
337
+ /** Null when the command was killed at its timeout. */
338
+ exitCode: Schema.NullOr(Schema.Int),
339
+ /** The tail of stdout, at most 1 MiB. */
340
+ stdout: Schema.String,
341
+ stderr: Schema.String,
342
+ truncated: Schema.Boolean,
343
+ timedOut: Schema.Boolean,
344
+ });
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export * from "./core-api/registries.js";
12
12
  export * from "./core-api/runs.js";
13
13
  export * from "./core-api/request-refusal.js";
14
14
  export * from "./core-api/resource-domains.js";
15
+ export * from "./core-api/resource-instances.js";
15
16
  export * from "./core-api/sessions.js";
16
17
  export * from "./core-api/access-tokens.js";
17
18
  export * from "./core-api/workspaces.js";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export * from "./core-api/registries.js";
12
12
  export * from "./core-api/runs.js";
13
13
  export * from "./core-api/request-refusal.js";
14
14
  export * from "./core-api/resource-domains.js";
15
+ export * from "./core-api/resource-instances.js";
15
16
  export * from "./core-api/sessions.js";
16
17
  export * from "./core-api/access-tokens.js";
17
18
  export * from "./core-api/workspaces.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/api-contracts-next",
3
- "version": "0.40.0-next.777",
3
+ "version": "0.40.0-next.783",
4
4
  "description": "Contract-first HTTP API definitions for Sealant services",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "peerDependencies": {
37
37
  "effect": "^4.0.5"
38
38
  },
39
- "gitHead": "e27067808ec70cbe18713aae91904e21dd7c534b",
39
+ "gitHead": "9d686294966f4b41d186efb8ee2ce27e34fdbe1b",
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  }