@smthrs/kernel 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +206 -0
- package/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/cjs/CapabilitySet.d.ts +6 -0
- package/dist/cjs/CapabilitySet.d.ts.map +1 -0
- package/dist/cjs/CapabilitySet.js +19 -0
- package/dist/cjs/CapabilitySet.js.map +7 -0
- package/dist/cjs/ChildProcessEnvironment.d.ts +60 -0
- package/dist/cjs/ChildProcessEnvironment.d.ts.map +1 -0
- package/dist/cjs/ChildProcessEnvironment.js +61 -0
- package/dist/cjs/ChildProcessEnvironment.js.map +7 -0
- package/dist/cjs/ChildProcessSpawner.d.ts +100 -0
- package/dist/cjs/ChildProcessSpawner.d.ts.map +1 -0
- package/dist/cjs/ChildProcessSpawner.js +291 -0
- package/dist/cjs/ChildProcessSpawner.js.map +7 -0
- package/dist/cjs/CommandLine.d.ts +173 -0
- package/dist/cjs/CommandLine.d.ts.map +1 -0
- package/dist/cjs/CommandLine.js +54 -0
- package/dist/cjs/CommandLine.js.map +7 -0
- package/dist/cjs/ContainedSpawner.d.ts +135 -0
- package/dist/cjs/ContainedSpawner.d.ts.map +1 -0
- package/dist/cjs/ContainedSpawner.js +141 -0
- package/dist/cjs/ContainedSpawner.js.map +7 -0
- package/dist/cjs/FileSystem.d.ts +452 -0
- package/dist/cjs/FileSystem.d.ts.map +1 -0
- package/dist/cjs/FileSystem.js +890 -0
- package/dist/cjs/FileSystem.js.map +7 -0
- package/dist/cjs/FileSystemBatch.d.ts +94 -0
- package/dist/cjs/FileSystemBatch.d.ts.map +1 -0
- package/dist/cjs/FileSystemBatch.js +31 -0
- package/dist/cjs/FileSystemBatch.js.map +7 -0
- package/dist/cjs/GrantEvent.d.ts +240 -0
- package/dist/cjs/GrantEvent.d.ts.map +1 -0
- package/dist/cjs/GrantEvent.js +115 -0
- package/dist/cjs/GrantEvent.js.map +7 -0
- package/dist/cjs/GrantStore.d.ts +270 -0
- package/dist/cjs/GrantStore.d.ts.map +1 -0
- package/dist/cjs/GrantStore.js +857 -0
- package/dist/cjs/GrantStore.js.map +7 -0
- package/dist/cjs/HostError.d.ts +36 -0
- package/dist/cjs/HostError.d.ts.map +1 -0
- package/dist/cjs/HostError.js +17 -0
- package/dist/cjs/HostError.js.map +7 -0
- package/dist/cjs/HostServices.d.ts +101 -0
- package/dist/cjs/HostServices.d.ts.map +1 -0
- package/dist/cjs/HostServices.js +66 -0
- package/dist/cjs/HostServices.js.map +7 -0
- package/dist/cjs/HttpClient.d.ts +177 -0
- package/dist/cjs/HttpClient.d.ts.map +1 -0
- package/dist/cjs/HttpClient.js +217 -0
- package/dist/cjs/HttpClient.js.map +7 -0
- package/dist/cjs/Jj.d.ts +67 -0
- package/dist/cjs/Jj.d.ts.map +1 -0
- package/dist/cjs/Jj.js +118 -0
- package/dist/cjs/Jj.js.map +7 -0
- package/dist/cjs/JournalGrantStore.d.ts +65 -0
- package/dist/cjs/JournalGrantStore.d.ts.map +1 -0
- package/dist/cjs/JournalGrantStore.js +354 -0
- package/dist/cjs/JournalGrantStore.js.map +7 -0
- package/dist/cjs/Path.d.ts +37 -0
- package/dist/cjs/Path.d.ts.map +1 -0
- package/dist/cjs/Path.js +46 -0
- package/dist/cjs/Path.js.map +7 -0
- package/dist/cjs/ProcessConfinement.d.ts +55 -0
- package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
- package/dist/cjs/ProcessConfinement.js +94 -0
- package/dist/cjs/ProcessConfinement.js.map +7 -0
- package/dist/cjs/ProcessLedger.d.ts +260 -0
- package/dist/cjs/ProcessLedger.d.ts.map +1 -0
- package/dist/cjs/ProcessLedger.js +190 -0
- package/dist/cjs/ProcessLedger.js.map +7 -0
- package/dist/cjs/Rooted.d.ts +53 -0
- package/dist/cjs/Rooted.d.ts.map +1 -0
- package/dist/cjs/Rooted.js +101 -0
- package/dist/cjs/Rooted.js.map +7 -0
- package/dist/cjs/Unreachable.d.ts +27 -0
- package/dist/cjs/Unreachable.d.ts.map +1 -0
- package/dist/cjs/Unreachable.js +43 -0
- package/dist/cjs/Unreachable.js.map +7 -0
- package/dist/cjs/Workspace.d.ts +62 -0
- package/dist/cjs/Workspace.d.ts.map +1 -0
- package/dist/cjs/Workspace.js +35 -0
- package/dist/cjs/Workspace.js.map +7 -0
- package/dist/cjs/index.d.ts +161 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/Containment.d.ts +24 -0
- package/dist/cjs/internal/Containment.d.ts.map +1 -0
- package/dist/cjs/internal/Containment.js +30 -0
- package/dist/cjs/internal/Containment.js.map +7 -0
- package/dist/cjs/internal/Pipeline.d.ts +18 -0
- package/dist/cjs/internal/Pipeline.d.ts.map +1 -0
- package/dist/cjs/internal/Pipeline.js +90 -0
- package/dist/cjs/internal/Pipeline.js.map +7 -0
- package/dist/cjs/internal/makeCapability.d.ts +16 -0
- package/dist/cjs/internal/makeCapability.d.ts.map +1 -0
- package/dist/cjs/internal/makeCapability.js +44 -0
- package/dist/cjs/internal/makeCapability.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/FileSystemBatchContract.d.ts +15 -0
- package/dist/cjs/test/FileSystemBatchContract.d.ts.map +1 -0
- package/dist/cjs/test/FileSystemBatchContract.js +76 -0
- package/dist/cjs/test/FileSystemBatchContract.js.map +7 -0
- package/dist/cjs/test/HostContract.d.ts +225 -0
- package/dist/cjs/test/HostContract.d.ts.map +1 -0
- package/dist/cjs/test/HostContract.js +489 -0
- package/dist/cjs/test/HostContract.js.map +7 -0
- package/dist/cjs/test/TestGrantStore.d.ts +31 -0
- package/dist/cjs/test/TestGrantStore.d.ts.map +1 -0
- package/dist/cjs/test/TestGrantStore.js +65 -0
- package/dist/cjs/test/TestGrantStore.js.map +7 -0
- package/dist/esm/CapabilitySet.d.ts +6 -0
- package/dist/esm/CapabilitySet.d.ts.map +1 -0
- package/dist/esm/CapabilitySet.js +6 -0
- package/dist/esm/CapabilitySet.js.map +1 -0
- package/dist/esm/ChildProcessEnvironment.d.ts +60 -0
- package/dist/esm/ChildProcessEnvironment.d.ts.map +1 -0
- package/dist/esm/ChildProcessEnvironment.js +91 -0
- package/dist/esm/ChildProcessEnvironment.js.map +1 -0
- package/dist/esm/ChildProcessSpawner.d.ts +100 -0
- package/dist/esm/ChildProcessSpawner.d.ts.map +1 -0
- package/dist/esm/ChildProcessSpawner.js +342 -0
- package/dist/esm/ChildProcessSpawner.js.map +1 -0
- package/dist/esm/CommandLine.d.ts +173 -0
- package/dist/esm/CommandLine.d.ts.map +1 -0
- package/dist/esm/CommandLine.js +195 -0
- package/dist/esm/CommandLine.js.map +1 -0
- package/dist/esm/ContainedSpawner.d.ts +135 -0
- package/dist/esm/ContainedSpawner.d.ts.map +1 -0
- package/dist/esm/ContainedSpawner.js +180 -0
- package/dist/esm/ContainedSpawner.js.map +1 -0
- package/dist/esm/FileSystem.d.ts +452 -0
- package/dist/esm/FileSystem.d.ts.map +1 -0
- package/dist/esm/FileSystem.js +989 -0
- package/dist/esm/FileSystem.js.map +1 -0
- package/dist/esm/FileSystemBatch.d.ts +94 -0
- package/dist/esm/FileSystemBatch.d.ts.map +1 -0
- package/dist/esm/FileSystemBatch.js +26 -0
- package/dist/esm/FileSystemBatch.js.map +1 -0
- package/dist/esm/GrantEvent.d.ts +240 -0
- package/dist/esm/GrantEvent.d.ts.map +1 -0
- package/dist/esm/GrantEvent.js +155 -0
- package/dist/esm/GrantEvent.js.map +1 -0
- package/dist/esm/GrantStore.d.ts +270 -0
- package/dist/esm/GrantStore.d.ts.map +1 -0
- package/dist/esm/GrantStore.js +951 -0
- package/dist/esm/GrantStore.js.map +1 -0
- package/dist/esm/HostError.d.ts +36 -0
- package/dist/esm/HostError.d.ts.map +1 -0
- package/dist/esm/HostError.js +20 -0
- package/dist/esm/HostError.js.map +1 -0
- package/dist/esm/HostServices.d.ts +101 -0
- package/dist/esm/HostServices.d.ts.map +1 -0
- package/dist/esm/HostServices.js +76 -0
- package/dist/esm/HostServices.js.map +1 -0
- package/dist/esm/HttpClient.d.ts +177 -0
- package/dist/esm/HttpClient.d.ts.map +1 -0
- package/dist/esm/HttpClient.js +299 -0
- package/dist/esm/HttpClient.js.map +1 -0
- package/dist/esm/Jj.d.ts +67 -0
- package/dist/esm/Jj.d.ts.map +1 -0
- package/dist/esm/Jj.js +111 -0
- package/dist/esm/Jj.js.map +1 -0
- package/dist/esm/JournalGrantStore.d.ts +65 -0
- package/dist/esm/JournalGrantStore.d.ts.map +1 -0
- package/dist/esm/JournalGrantStore.js +350 -0
- package/dist/esm/JournalGrantStore.js.map +1 -0
- package/dist/esm/Path.d.ts +37 -0
- package/dist/esm/Path.d.ts.map +1 -0
- package/dist/esm/Path.js +34 -0
- package/dist/esm/Path.js.map +1 -0
- package/dist/esm/ProcessConfinement.d.ts +55 -0
- package/dist/esm/ProcessConfinement.d.ts.map +1 -0
- package/dist/esm/ProcessConfinement.js +106 -0
- package/dist/esm/ProcessConfinement.js.map +1 -0
- package/dist/esm/ProcessLedger.d.ts +260 -0
- package/dist/esm/ProcessLedger.d.ts.map +1 -0
- package/dist/esm/ProcessLedger.js +283 -0
- package/dist/esm/ProcessLedger.js.map +1 -0
- package/dist/esm/Rooted.d.ts +53 -0
- package/dist/esm/Rooted.d.ts.map +1 -0
- package/dist/esm/Rooted.js +104 -0
- package/dist/esm/Rooted.js.map +1 -0
- package/dist/esm/Unreachable.d.ts +27 -0
- package/dist/esm/Unreachable.d.ts.map +1 -0
- package/dist/esm/Unreachable.js +26 -0
- package/dist/esm/Unreachable.js.map +1 -0
- package/dist/esm/Workspace.d.ts +62 -0
- package/dist/esm/Workspace.d.ts.map +1 -0
- package/dist/esm/Workspace.js +51 -0
- package/dist/esm/Workspace.js.map +1 -0
- package/dist/esm/index.d.ts +161 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +151 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/Containment.d.ts +24 -0
- package/dist/esm/internal/Containment.d.ts.map +1 -0
- package/dist/esm/internal/Containment.js +27 -0
- package/dist/esm/internal/Containment.js.map +1 -0
- package/dist/esm/internal/Pipeline.d.ts +18 -0
- package/dist/esm/internal/Pipeline.d.ts.map +1 -0
- package/dist/esm/internal/Pipeline.js +68 -0
- package/dist/esm/internal/Pipeline.js.map +1 -0
- package/dist/esm/internal/makeCapability.d.ts +16 -0
- package/dist/esm/internal/makeCapability.d.ts.map +1 -0
- package/dist/esm/internal/makeCapability.js +22 -0
- package/dist/esm/internal/makeCapability.js.map +1 -0
- package/dist/esm/test/FileSystemBatchContract.d.ts +15 -0
- package/dist/esm/test/FileSystemBatchContract.d.ts.map +1 -0
- package/dist/esm/test/FileSystemBatchContract.js +57 -0
- package/dist/esm/test/FileSystemBatchContract.js.map +1 -0
- package/dist/esm/test/HostContract.d.ts +225 -0
- package/dist/esm/test/HostContract.d.ts.map +1 -0
- package/dist/esm/test/HostContract.js +460 -0
- package/dist/esm/test/HostContract.js.map +1 -0
- package/dist/esm/test/TestGrantStore.d.ts +31 -0
- package/dist/esm/test/TestGrantStore.d.ts.map +1 -0
- package/dist/esm/test/TestGrantStore.js +57 -0
- package/dist/esm/test/TestGrantStore.js.map +1 -0
- package/docs/README.md +162 -0
- package/docs/api.md +1532 -0
- package/docs/concepts/decoration-in-place.md +117 -0
- package/docs/concepts/filesystem-batches.md +58 -0
- package/docs/concepts/filesystem-confinement.md +168 -0
- package/docs/concepts/grant-decisions.md +181 -0
- package/docs/concepts/process-containment.md +155 -0
- package/docs/contract.md +14 -0
- package/docs/guides/adapt-a-new-host-platform.md +148 -0
- package/docs/guides/answer-permission-requests.md +151 -0
- package/docs/guides/authorize-network-and-model-calls.md +118 -0
- package/docs/guides/contain-spawned-processes.md +118 -0
- package/docs/guides/guard-a-host-bundle.md +109 -0
- package/docs/guides/persist-grants-across-restarts.md +135 -0
- package/docs/guides/write-a-capability-policy.md +159 -0
- package/docs/installation.md +115 -0
- package/docs/quickstart.md +182 -0
- package/docs/testing.md +108 -0
- package/docs/troubleshooting.md +281 -0
- package/package.json +344 -3
- package/src/CapabilitySet.ts +6 -0
- package/src/ChildProcessEnvironment.ts +98 -0
- package/src/ChildProcessSpawner.ts +414 -0
- package/src/CommandLine.ts +250 -0
- package/src/ContainedSpawner.ts +284 -0
- package/src/FileSystem.ts +1655 -0
- package/src/FileSystemBatch.ts +90 -0
- package/src/GrantEvent.ts +174 -0
- package/src/GrantStore.ts +1345 -0
- package/src/HostError.ts +37 -0
- package/src/HostServices.ts +133 -0
- package/src/HttpClient.ts +406 -0
- package/src/Jj.ts +159 -0
- package/src/JournalGrantStore.ts +461 -0
- package/src/Path.ts +47 -0
- package/src/ProcessConfinement.ts +156 -0
- package/src/ProcessLedger.ts +423 -0
- package/src/Rooted.ts +128 -0
- package/src/Unreachable.ts +30 -0
- package/src/Workspace.ts +65 -0
- package/src/index.ts +181 -0
- package/src/internal/Containment.ts +37 -0
- package/src/internal/Pipeline.ts +85 -0
- package/src/internal/makeCapability.ts +26 -0
- package/src/test/FileSystemBatchContract.ts +59 -0
- package/src/test/HostContract.ts +785 -0
- package/src/test/TestGrantStore.ts +73 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory capability grants with an attended Deferred request lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Governing design:
|
|
5
|
+
* `docs/specs/Concepts/Permission Kernel.md` and
|
|
6
|
+
* `docs/specs/Concepts/Trust Granularity.md`.
|
|
7
|
+
*
|
|
8
|
+
* @since 1.0.0-rc.0
|
|
9
|
+
*/
|
|
10
|
+
import { Capability as CapabilityValue, CapabilityPattern, format, matches, maxResourceLength, parsePattern, patternFromCapability, subsumes, tierOf } from "@smthrs/capability/Capability";
|
|
11
|
+
import { evaluate, GrantStoreError, permissionDenied, permissionRequired, Rule } from "@smthrs/capability/Permission";
|
|
12
|
+
import { digestSync } from "@smthrs/crypto/Sha256";
|
|
13
|
+
import { Cause, Context, Deferred, Effect, Layer, Option, Schema, Semaphore } from "effect";
|
|
14
|
+
import { allows, current, fromPatterns, intersect } from "./CapabilitySet.js";
|
|
15
|
+
import { DeniedGrant, EnvelopeGrant, OnceGrant, RememberedGrant, RunGrant } from "./GrantEvent.js";
|
|
16
|
+
import { Workspace } from "./Workspace.js";
|
|
17
|
+
/** A conservative snapshot for services that expose no durable authority.
|
|
18
|
+
* @category constructors
|
|
19
|
+
* @since 1.0.0-rc.1
|
|
20
|
+
*/
|
|
21
|
+
export const emptyPolicy = current.pipe(Effect.map((ceiling) => Object.freeze({ groups: Object.freeze([]), ceiling })));
|
|
22
|
+
/**
|
|
23
|
+
* Service key for permission decisions and attended grant requests.
|
|
24
|
+
*
|
|
25
|
+
* @category services
|
|
26
|
+
* @since 1.0.0-rc.0
|
|
27
|
+
*/
|
|
28
|
+
export class GrantStore extends Context.Service()("@smthrs/kernel/GrantStore") {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Maximum policy rules retained by one store.
|
|
32
|
+
* @category limits
|
|
33
|
+
* @since 1.0.0-rc.0
|
|
34
|
+
*/
|
|
35
|
+
export const maximumRules = 1_024;
|
|
36
|
+
/**
|
|
37
|
+
* Maximum predicates in one approval envelope.
|
|
38
|
+
* @category limits
|
|
39
|
+
* @since 1.0.0-rc.0
|
|
40
|
+
*/
|
|
41
|
+
export const maximumEnvelopePatterns = 256;
|
|
42
|
+
/**
|
|
43
|
+
* Maximum permission requests parked at once.
|
|
44
|
+
* @category limits
|
|
45
|
+
* @since 1.0.0-rc.0
|
|
46
|
+
*/
|
|
47
|
+
export const maximumPendingRequests = 1_024;
|
|
48
|
+
/**
|
|
49
|
+
* Maximum nesting depth of permission metadata.
|
|
50
|
+
* @category limits
|
|
51
|
+
* @since 1.0.0-rc.0
|
|
52
|
+
*/
|
|
53
|
+
export const maximumMetadataDepth = 16;
|
|
54
|
+
/**
|
|
55
|
+
* Maximum members across one permission metadata graph.
|
|
56
|
+
* @category limits
|
|
57
|
+
* @since 1.0.0-rc.0
|
|
58
|
+
*/
|
|
59
|
+
export const maximumMetadataMembers = 1_024;
|
|
60
|
+
/**
|
|
61
|
+
* Maximum encoded bytes retained for one metadata value.
|
|
62
|
+
* @category limits
|
|
63
|
+
* @since 1.0.0-rc.0
|
|
64
|
+
*/
|
|
65
|
+
export const maximumMetadataBytes = 64 * 1024;
|
|
66
|
+
/**
|
|
67
|
+
* Maximum encoded bytes persisted for one grant event.
|
|
68
|
+
* @category limits
|
|
69
|
+
* @since 1.0.0-rc.0
|
|
70
|
+
*/
|
|
71
|
+
export const maximumEventBytes = 256 * 1024;
|
|
72
|
+
/**
|
|
73
|
+
* Maximum milliseconds one grant journal write may take before its reply or
|
|
74
|
+
* envelope admission fails with `journal_failed`. Journal writes never hold
|
|
75
|
+
* the store's mutation permit, so a slow write stalls only the admission that
|
|
76
|
+
* issued it.
|
|
77
|
+
* @category limits
|
|
78
|
+
* @since 1.0.0-rc.0
|
|
79
|
+
*/
|
|
80
|
+
export const maximumPersistMillis = 30_000;
|
|
81
|
+
/**
|
|
82
|
+
* Maximum length of a run, plan, request, or signature identity.
|
|
83
|
+
* @category limits
|
|
84
|
+
* @since 1.0.0-rc.0
|
|
85
|
+
*/
|
|
86
|
+
export const maximumIdentityLength = 4_096;
|
|
87
|
+
/**
|
|
88
|
+
* Maximum capability resource length enforced by the capability vocabulary.
|
|
89
|
+
* @category limits
|
|
90
|
+
* @since 1.0.0-rc.0
|
|
91
|
+
*/
|
|
92
|
+
export const maximumCapabilityResourceLength = maxResourceLength;
|
|
93
|
+
const encoder = new TextEncoder();
|
|
94
|
+
const isWellFormed = (value) => {
|
|
95
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
96
|
+
const unit = value.charCodeAt(index);
|
|
97
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
98
|
+
const next = value.charCodeAt(index + 1);
|
|
99
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
100
|
+
return false;
|
|
101
|
+
index += 1;
|
|
102
|
+
}
|
|
103
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
};
|
|
109
|
+
const invalid = (message) => new GrantStoreError({ code: "invalid_resolution", message });
|
|
110
|
+
const immutableFields = (value, fields) => {
|
|
111
|
+
for (const field of fields) {
|
|
112
|
+
Object.defineProperty(value, field, {
|
|
113
|
+
value: value[field],
|
|
114
|
+
enumerable: true,
|
|
115
|
+
writable: false,
|
|
116
|
+
configurable: false
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
};
|
|
121
|
+
const snapshotCapability = (capability) => immutableFields(new CapabilityValue({ action: capability.action, resource: capability.resource }), ["action", "resource"]);
|
|
122
|
+
const snapshotPattern = (pattern) => immutableFields(new CapabilityPattern({ action: pattern.action, resource: pattern.resource }), ["action", "resource"]);
|
|
123
|
+
const snapshotRule = (rule) => immutableFields(new Rule({ effect: rule.effect, pattern: snapshotPattern(rule.pattern) }), ["effect", "pattern"]);
|
|
124
|
+
const snapshotFailure = (what, cause) => cause instanceof GrantStoreError ? cause : invalid(`${what} is invalid`);
|
|
125
|
+
const attemptSnapshot = (what, evaluate) => Effect.try({
|
|
126
|
+
try: evaluate,
|
|
127
|
+
catch: (cause) => snapshotFailure(what, cause)
|
|
128
|
+
});
|
|
129
|
+
const metadataSnapshot = (input) => {
|
|
130
|
+
let members = 0;
|
|
131
|
+
const active = new WeakSet();
|
|
132
|
+
const visit = (value, depth) => {
|
|
133
|
+
if (depth > maximumMetadataDepth)
|
|
134
|
+
throw invalid(`metadata exceeds depth ${maximumMetadataDepth}`);
|
|
135
|
+
if (value === null || typeof value === "boolean")
|
|
136
|
+
return value;
|
|
137
|
+
if (typeof value === "number") {
|
|
138
|
+
if (!Number.isFinite(value))
|
|
139
|
+
throw invalid("metadata contains a non-finite number");
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value === "string") {
|
|
143
|
+
if (!isWellFormed(value))
|
|
144
|
+
throw invalid("metadata contains ill-formed text");
|
|
145
|
+
if (value.length > maximumMetadataBytes)
|
|
146
|
+
throw invalid(`metadata exceeds ${maximumMetadataBytes} bytes`);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value !== "object" || value === null)
|
|
150
|
+
throw invalid("metadata must be JSON data");
|
|
151
|
+
if (active.has(value))
|
|
152
|
+
throw invalid("metadata must not contain cycles");
|
|
153
|
+
active.add(value);
|
|
154
|
+
try {
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
157
|
+
const snapshot = [];
|
|
158
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
159
|
+
members += 1;
|
|
160
|
+
if (members > maximumMetadataMembers) {
|
|
161
|
+
throw invalid(`metadata exceeds ${maximumMetadataMembers} members`);
|
|
162
|
+
}
|
|
163
|
+
const descriptor = descriptors[String(index)];
|
|
164
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
165
|
+
throw invalid("metadata arrays must be dense data");
|
|
166
|
+
}
|
|
167
|
+
snapshot.push(visit(descriptor.value, depth + 1));
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze(snapshot);
|
|
170
|
+
}
|
|
171
|
+
const prototype = Object.getPrototypeOf(value);
|
|
172
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
173
|
+
throw invalid("metadata objects must be plain records");
|
|
174
|
+
}
|
|
175
|
+
const snapshot = Object.create(null);
|
|
176
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
177
|
+
if (!descriptor.enumerable)
|
|
178
|
+
continue;
|
|
179
|
+
members += 1;
|
|
180
|
+
if (members > maximumMetadataMembers) {
|
|
181
|
+
throw invalid(`metadata exceeds ${maximumMetadataMembers} members`);
|
|
182
|
+
}
|
|
183
|
+
if (!isWellFormed(key) || !("value" in descriptor)) {
|
|
184
|
+
throw invalid("metadata members must be well-formed data properties");
|
|
185
|
+
}
|
|
186
|
+
if (descriptor.value !== undefined)
|
|
187
|
+
snapshot[key] = visit(descriptor.value, depth + 1);
|
|
188
|
+
}
|
|
189
|
+
return Object.freeze(snapshot);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
active.delete(value);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const snapshot = visit(input, 0);
|
|
196
|
+
if (Array.isArray(snapshot) || snapshot === null || typeof snapshot !== "object") {
|
|
197
|
+
throw invalid("metadata must be a record");
|
|
198
|
+
}
|
|
199
|
+
const encoded = JSON.stringify(snapshot);
|
|
200
|
+
if (encoder.encode(encoded).byteLength > maximumMetadataBytes) {
|
|
201
|
+
throw invalid(`metadata exceeds ${maximumMetadataBytes} bytes`);
|
|
202
|
+
}
|
|
203
|
+
return snapshot;
|
|
204
|
+
};
|
|
205
|
+
const validIdentity = (value) => value === undefined ||
|
|
206
|
+
(typeof value === "string" &&
|
|
207
|
+
value.length > 0 &&
|
|
208
|
+
value.length <= maximumIdentityLength &&
|
|
209
|
+
isWellFormed(value) &&
|
|
210
|
+
!value.includes("\0"));
|
|
211
|
+
const prepareEnvelope = (input, expectedPlanDigest, workspaceRoot) => attemptSnapshot("grant envelope", () => {
|
|
212
|
+
if (!Array.isArray(input.patterns))
|
|
213
|
+
throw invalid("patterns must be an array");
|
|
214
|
+
if (input.patterns.length > maximumEnvelopePatterns) {
|
|
215
|
+
throw invalid(`patterns exceed ${maximumEnvelopePatterns} entries`);
|
|
216
|
+
}
|
|
217
|
+
const patterns = canonicalEnvelopePatterns(input.patterns);
|
|
218
|
+
// An empty envelope carries no authority. It is deliberately a no-op even
|
|
219
|
+
// when replayed without the plan identity that authorized a non-empty one.
|
|
220
|
+
if (patterns.length === 0) {
|
|
221
|
+
return { planDigest: expectedPlanDigest ?? "", scope: "run", patterns };
|
|
222
|
+
}
|
|
223
|
+
const scope = input.scope ?? "run";
|
|
224
|
+
if (!isEnvelopeScope(scope))
|
|
225
|
+
throw invalid("scope must be run or remembered");
|
|
226
|
+
if (expectedPlanDigest === undefined)
|
|
227
|
+
throw invalid("grant envelope requires a configured planDigest");
|
|
228
|
+
if (!validIdentity(input.planDigest))
|
|
229
|
+
throw invalid("planDigest is empty, malformed, or too long");
|
|
230
|
+
if (input.planDigest !== expectedPlanDigest)
|
|
231
|
+
throw invalid("planDigest mismatch");
|
|
232
|
+
for (let index = 0; index < patterns.length; index += 1) {
|
|
233
|
+
if (!isValidEnvelopePattern(patterns[index], workspaceRoot)) {
|
|
234
|
+
throw invalid(`patterns[${index}] is outside the workspace envelope`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { planDigest: input.planDigest, scope, patterns };
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* Records the displayed capability for once/deny audit events, which are never
|
|
241
|
+
* installed or replayed as authority.
|
|
242
|
+
*/
|
|
243
|
+
const exactPattern = (capability) => new CapabilityPattern({
|
|
244
|
+
action: capability.action,
|
|
245
|
+
resource: capability.resource
|
|
246
|
+
});
|
|
247
|
+
const hasResourceWildcard = (resource) => resource.includes("*") || resource.includes("?");
|
|
248
|
+
const isResolution = (value) => value === "once" || value === "run" || value === "remembered" || value === "deny";
|
|
249
|
+
const isEnvelopeScope = (value) => value === "run" || value === "remembered";
|
|
250
|
+
/**
|
|
251
|
+
* Canonicalizes an envelope pattern list: duplicate predicates collapse to
|
|
252
|
+
* their first occurrence and the survivors sort by their formatted identity.
|
|
253
|
+
*
|
|
254
|
+
* An envelope is a *set* of predicates, so two envelopes listing the same
|
|
255
|
+
* predicates in a different order or multiplicity are the same approval. Every
|
|
256
|
+
* envelope is canonicalized before it is persisted or compared, which makes
|
|
257
|
+
* envelope idempotency structural rather than dependent on caller discipline.
|
|
258
|
+
*
|
|
259
|
+
* The ordering is the code-unit sort of `format(pattern)` — the one renderer
|
|
260
|
+
* for capability identity. RFC 8785 canonical JSON (`@smthrs/canonical`)
|
|
261
|
+
* was considered and does not fit: it canonicalizes object keys but preserves
|
|
262
|
+
* array order as semantic, and the ordering that matters here is exactly the
|
|
263
|
+
* pattern-array order.
|
|
264
|
+
*
|
|
265
|
+
* @category validation
|
|
266
|
+
* @since 1.0.0-rc.0
|
|
267
|
+
*/
|
|
268
|
+
export const canonicalEnvelopePatterns = (patterns) => {
|
|
269
|
+
const byIdentity = new Map();
|
|
270
|
+
for (const input of patterns) {
|
|
271
|
+
const pattern = snapshotPattern(input);
|
|
272
|
+
const identity = format(pattern);
|
|
273
|
+
if (!byIdentity.has(identity)) {
|
|
274
|
+
byIdentity.set(identity, pattern);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return Object.freeze([...byIdentity.keys()].sort().map((identity) => byIdentity.get(identity)));
|
|
278
|
+
};
|
|
279
|
+
const envelopeEncoding = (planDigest, scope, patterns) => JSON.stringify({ planDigest, scope, patterns: canonicalEnvelopePatterns(patterns).map(format) });
|
|
280
|
+
/**
|
|
281
|
+
* Computes the canonical identity of an envelope approval.
|
|
282
|
+
*
|
|
283
|
+
* Two envelopes with the same plan digest, scope, and predicate set produce
|
|
284
|
+
* the same signature regardless of pattern order or repetition. The signature
|
|
285
|
+
* is how the store, and journal replay above it, recognise an envelope that
|
|
286
|
+
* is already durable. It is `sha256:` followed by 64 lowercase hex digits.
|
|
287
|
+
*
|
|
288
|
+
* @category validation
|
|
289
|
+
* @since 1.0.0-rc.0
|
|
290
|
+
*/
|
|
291
|
+
export const envelopeSignature = (planDigest, scope, patterns) => `sha256:${digestSync(envelopeEncoding(planDigest, scope, patterns))}`;
|
|
292
|
+
const decodeLegacySignature = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Struct({
|
|
293
|
+
planDigest: Schema.String,
|
|
294
|
+
scope: Schema.Literals(["run", "remembered"]),
|
|
295
|
+
patterns: Schema.Array(Schema.String).check(Schema.isMaxLength(maximumEnvelopePatterns))
|
|
296
|
+
})), { onExcessProperty: "error" });
|
|
297
|
+
// v1 events contain predicates, not signatures, and replay hashes them afresh.
|
|
298
|
+
// Also accept canonical full-JSON identities supplied by older seed callers.
|
|
299
|
+
const normalizeEnvelopeSignature = (value) => {
|
|
300
|
+
if (!value.startsWith("{"))
|
|
301
|
+
return value;
|
|
302
|
+
if (value.length > maximumEventBytes || encoder.encode(value).byteLength > maximumEventBytes)
|
|
303
|
+
return value;
|
|
304
|
+
const decoded = decodeLegacySignature(value);
|
|
305
|
+
if (decoded._tag === "Failure" || !validIdentity(decoded.success.planDigest))
|
|
306
|
+
return value;
|
|
307
|
+
const { planDigest, scope, patterns } = decoded.success;
|
|
308
|
+
const parsed = Option.all(patterns.map(parsePattern));
|
|
309
|
+
if (Option.isNone(parsed) || envelopeEncoding(planDigest, scope, parsed.value) !== value)
|
|
310
|
+
return value;
|
|
311
|
+
return envelopeSignature(planDigest, scope, parsed.value);
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Checks that a request-scoped grant cannot authorize a different action or a
|
|
315
|
+
* more dangerous effect tier than the request displayed to the user. A
|
|
316
|
+
* wildcard-bearing pattern identical to the resource is ambiguous because the
|
|
317
|
+
* grammar has no escape, so its wildcard reading would silently widen access.
|
|
318
|
+
*
|
|
319
|
+
* @category validation
|
|
320
|
+
* @since 1.0.0-rc.0
|
|
321
|
+
*/
|
|
322
|
+
export const isValidGrantPattern = (pattern, capability, tier, workspaceRoot) => {
|
|
323
|
+
if (pattern.action !== capability.action || !matches(pattern, capability)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (hasResourceWildcard(pattern.resource) && pattern.resource === capability.resource) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (tierOf(capability, { workspaceRoot }) !== tier) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (capability.action !== "fs:write") {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (tier === "irreversible") {
|
|
336
|
+
return !hasResourceWildcard(pattern.resource);
|
|
337
|
+
}
|
|
338
|
+
const workspacePattern = new CapabilityPattern({
|
|
339
|
+
action: "fs:write",
|
|
340
|
+
resource: `${workspaceRoot.replace(/[\\/]+$/, "")}/**`
|
|
341
|
+
});
|
|
342
|
+
return pattern.resource === workspaceRoot || subsumes(workspacePattern, pattern);
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Checks that a bulk envelope pattern preserves exact action and filesystem
|
|
346
|
+
* effect-tier boundaries.
|
|
347
|
+
*
|
|
348
|
+
* @category validation
|
|
349
|
+
* @since 1.0.0-rc.0
|
|
350
|
+
*/
|
|
351
|
+
export const isValidEnvelopePattern = (pattern, workspaceRoot) => {
|
|
352
|
+
if (pattern.action.includes("*")) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (pattern.action !== "fs:write" || !hasResourceWildcard(pattern.resource)) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
const workspacePattern = new CapabilityPattern({
|
|
359
|
+
action: "fs:write",
|
|
360
|
+
resource: `${workspaceRoot.replace(/[\\/]+$/, "")}/**`
|
|
361
|
+
});
|
|
362
|
+
return subsumes(workspacePattern, pattern);
|
|
363
|
+
};
|
|
364
|
+
const normalizeRules = (rules) => {
|
|
365
|
+
if (rules === undefined || rules.length === 0 || rules[0] === undefined) {
|
|
366
|
+
return { configured: [], remembered: [] };
|
|
367
|
+
}
|
|
368
|
+
if (rules.length > maximumRules)
|
|
369
|
+
throw invalid(`rules exceed ${maximumRules} entries`);
|
|
370
|
+
if (Array.isArray(rules[0])) {
|
|
371
|
+
const rulesets = rules;
|
|
372
|
+
let count = 0;
|
|
373
|
+
for (const ruleset of rulesets) {
|
|
374
|
+
count += ruleset.length;
|
|
375
|
+
if (count > maximumRules)
|
|
376
|
+
throw invalid(`rules exceed ${maximumRules} entries`);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
configured: Object.freeze(rulesets[0].map(snapshotRule)),
|
|
380
|
+
remembered: Object.freeze(rulesets.slice(1).flatMap((ruleset) => ruleset.map(snapshotRule)))
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
configured: Object.freeze(rules.map(snapshotRule)),
|
|
385
|
+
remembered: []
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Builds an in-memory grant store.
|
|
390
|
+
*
|
|
391
|
+
* Attended stores suspend an asking fiber on a request-local `Deferred`.
|
|
392
|
+
* Unattended stores fail immediately with `PermissionRequired`. The returned
|
|
393
|
+
* service is scoped: closing its scope rejects every waiter and clears the
|
|
394
|
+
* pending map.
|
|
395
|
+
*
|
|
396
|
+
* @category constructors
|
|
397
|
+
* @since 1.0.0-rc.0
|
|
398
|
+
*/
|
|
399
|
+
export const make = (options = {}) => Effect.gen(function* () {
|
|
400
|
+
const attended = options.attended ?? true;
|
|
401
|
+
const runId = options.runId;
|
|
402
|
+
const planDigest = options.planDigest;
|
|
403
|
+
if (!validIdentity(runId)) {
|
|
404
|
+
return yield* Effect.fail(invalid("runId is empty, malformed, or too long"));
|
|
405
|
+
}
|
|
406
|
+
if (!validIdentity(planDigest)) {
|
|
407
|
+
return yield* Effect.fail(invalid("planDigest is empty, malformed, or too long"));
|
|
408
|
+
}
|
|
409
|
+
const workspace = yield* Workspace;
|
|
410
|
+
const initialCeiling = yield* current;
|
|
411
|
+
const workspaceRoot = workspace.root;
|
|
412
|
+
const initial = yield* attemptSnapshot("rules", () => normalizeRules(options.rules));
|
|
413
|
+
const initialRunRules = yield* attemptSnapshot("runRules", () => {
|
|
414
|
+
const values = options.runRules ?? [];
|
|
415
|
+
if (values.length > maximumRules)
|
|
416
|
+
throw invalid(`runRules exceed ${maximumRules} entries`);
|
|
417
|
+
return values.map((value) => "rule" in value
|
|
418
|
+
? {
|
|
419
|
+
rule: snapshotRule(value.rule),
|
|
420
|
+
ceiling: value.ceiling.reduce((ceiling, group) => intersect(ceiling, fromPatterns(group)), initialCeiling)
|
|
421
|
+
}
|
|
422
|
+
: { rule: snapshotRule(value), ceiling: initialCeiling });
|
|
423
|
+
});
|
|
424
|
+
if (initial.configured.length + initial.remembered.length + initialRunRules.length > maximumRules) {
|
|
425
|
+
return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`));
|
|
426
|
+
}
|
|
427
|
+
const configuredRules = initial.configured;
|
|
428
|
+
const envelopeRules = [];
|
|
429
|
+
const runRules = initialRunRules;
|
|
430
|
+
const rememberedRules = [...initial.remembered];
|
|
431
|
+
const pending = new Map();
|
|
432
|
+
const persist = options.persist ?? (() => Effect.void);
|
|
433
|
+
const signatures = yield* attemptSnapshot("envelopeSignatures", () => {
|
|
434
|
+
const values = options.envelopeSignatures ?? [];
|
|
435
|
+
if (!Array.isArray(values) || values.length > maximumRules) {
|
|
436
|
+
throw invalid(`envelopeSignatures exceed ${maximumRules} entries`);
|
|
437
|
+
}
|
|
438
|
+
return values.map((value, index) => {
|
|
439
|
+
const signature = typeof value === "string" ? normalizeEnvelopeSignature(value) : value;
|
|
440
|
+
if (typeof signature !== "string" || !validIdentity(signature)) {
|
|
441
|
+
throw invalid(`envelopeSignatures[${index}] is malformed or too long`);
|
|
442
|
+
}
|
|
443
|
+
return signature;
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
const grantedEnvelopes = new Set(signatures);
|
|
447
|
+
let nextRequestId = 1;
|
|
448
|
+
let closed = false;
|
|
449
|
+
const mutation = yield* Semaphore.make(1);
|
|
450
|
+
const persistEvent = (event) => {
|
|
451
|
+
// Every event is constructed below exclusively from validated primitive
|
|
452
|
+
// identities and immutable capability snapshots, so encoding cannot
|
|
453
|
+
// observe caller objects or invoke a caller-defined `toJSON` hook.
|
|
454
|
+
const bytes = encoder.encode(JSON.stringify(event)).byteLength;
|
|
455
|
+
return bytes > maximumEventBytes
|
|
456
|
+
? Effect.fail(invalid(`grant event exceeds ${maximumEventBytes} bytes`))
|
|
457
|
+
: Effect.timeoutOrElse(persist(event), {
|
|
458
|
+
duration: maximumPersistMillis,
|
|
459
|
+
orElse: () => Effect.fail(new GrantStoreError({
|
|
460
|
+
code: "journal_failed",
|
|
461
|
+
message: `grant journal write exceeded ${maximumPersistMillis}ms`
|
|
462
|
+
}))
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
const invalidRemembered = rememberedRules.findIndex((rule) => rule.effect === "allow" && !isValidEnvelopePattern(rule.pattern, workspaceRoot));
|
|
466
|
+
if (invalidRemembered !== -1) {
|
|
467
|
+
return yield* Effect.fail(invalid(`rememberedRules[${invalidRemembered}] is outside the workspace envelope`));
|
|
468
|
+
}
|
|
469
|
+
const invalidRun = runRules.findIndex(({ rule }) => rule.effect === "allow" && !isValidEnvelopePattern(rule.pattern, workspaceRoot));
|
|
470
|
+
if (invalidRun !== -1) {
|
|
471
|
+
return yield* Effect.fail(invalid(`runRules[${invalidRun}] is outside the workspace envelope`));
|
|
472
|
+
}
|
|
473
|
+
// Construction envelopes validate capacity before persistence or
|
|
474
|
+
// activation. A seeded construction envelope may already have rules
|
|
475
|
+
// installed by replay. Runtime admission is two-phase in grantEnvelope.
|
|
476
|
+
const admitEnvelope = (envelope) => Effect.gen(function* () {
|
|
477
|
+
const { patterns, scope } = envelope;
|
|
478
|
+
if (patterns.length === 0)
|
|
479
|
+
return false;
|
|
480
|
+
const signature = envelopeSignature(envelope.planDigest, scope, patterns);
|
|
481
|
+
const durable = grantedEnvelopes.has(signature);
|
|
482
|
+
const destination = scope === "remembered" ? rememberedRules : envelopeRules;
|
|
483
|
+
const replayed = scope === "remembered"
|
|
484
|
+
? rememberedRules
|
|
485
|
+
: runRules.filter(({ ceiling }) => ceiling === initialCeiling).map(({ rule }) => rule);
|
|
486
|
+
const existing = new Set();
|
|
487
|
+
for (const rule of replayed) {
|
|
488
|
+
// Supplied rules can mask earlier grants. Only the trailing allow
|
|
489
|
+
// rules (including journal replay) can replace a fresh activation.
|
|
490
|
+
if (rule.effect === "allow")
|
|
491
|
+
existing.add(format(rule.pattern));
|
|
492
|
+
else
|
|
493
|
+
existing.clear();
|
|
494
|
+
}
|
|
495
|
+
const incoming = durable ? patterns.filter((pattern) => !existing.has(format(pattern))) : patterns;
|
|
496
|
+
if (configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
|
|
497
|
+
incoming.length > maximumRules) {
|
|
498
|
+
return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`));
|
|
499
|
+
}
|
|
500
|
+
if (!durable && grantedEnvelopes.size >= maximumRules) {
|
|
501
|
+
return yield* Effect.fail(invalid(`grant envelopes exceed ${maximumRules} entries`));
|
|
502
|
+
}
|
|
503
|
+
if (!durable) {
|
|
504
|
+
yield* persistEvent(new EnvelopeGrant({
|
|
505
|
+
eventType: "flows.kernel.grant.envelope.v1",
|
|
506
|
+
runId: runId ?? "",
|
|
507
|
+
planDigest: envelope.planDigest,
|
|
508
|
+
patterns,
|
|
509
|
+
scope
|
|
510
|
+
}));
|
|
511
|
+
grantedEnvelopes.add(signature);
|
|
512
|
+
}
|
|
513
|
+
for (const pattern of incoming) {
|
|
514
|
+
destination.push(snapshotRule(new Rule({ effect: "allow", pattern })));
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
});
|
|
518
|
+
if (options.envelope !== undefined) {
|
|
519
|
+
const envelope = yield* prepareEnvelope(options.envelope, planDigest, workspaceRoot);
|
|
520
|
+
yield* admitEnvelope(envelope);
|
|
521
|
+
}
|
|
522
|
+
const rulesets = (capability) => [
|
|
523
|
+
configuredRules,
|
|
524
|
+
envelopeRules,
|
|
525
|
+
runRules
|
|
526
|
+
.filter(({ ceiling }) => allows(ceiling, capability))
|
|
527
|
+
.map(({ rule }) => rule),
|
|
528
|
+
rememberedRules
|
|
529
|
+
];
|
|
530
|
+
yield* Effect.addFinalizer(() => mutation.withPermit(Effect.gen(function* () {
|
|
531
|
+
closed = true;
|
|
532
|
+
const entries = [...pending.values()];
|
|
533
|
+
pending.clear();
|
|
534
|
+
yield* Effect.forEach(entries, (entry) => Deferred.fail(entry.deferred, permissionDenied(entry.capability, "grant store closed")), { discard: true });
|
|
535
|
+
})));
|
|
536
|
+
const allocateRequest = (capability, tier, meta, ceiling) => Effect.gen(function* () {
|
|
537
|
+
if (pending.size >= maximumPendingRequests) {
|
|
538
|
+
return yield* Effect.fail(invalid(`pending requests exceed ${maximumPendingRequests} entries`));
|
|
539
|
+
}
|
|
540
|
+
const requestId = `permission-${nextRequestId++}`;
|
|
541
|
+
const deferred = yield* Deferred.make();
|
|
542
|
+
const entry = {
|
|
543
|
+
requestId,
|
|
544
|
+
capability,
|
|
545
|
+
tier,
|
|
546
|
+
meta,
|
|
547
|
+
ceiling,
|
|
548
|
+
deferred
|
|
549
|
+
};
|
|
550
|
+
pending.set(requestId, entry);
|
|
551
|
+
return entry;
|
|
552
|
+
});
|
|
553
|
+
const nextUnattendedRequestId = Effect.sync(() => `permission-${nextRequestId++}`);
|
|
554
|
+
const check = Effect.fn("GrantStore.check")((capability, meta = {}) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
555
|
+
const request = yield* attemptSnapshot("permission request", () => ({
|
|
556
|
+
capability: snapshotCapability(capability),
|
|
557
|
+
meta: metadataSnapshot(meta)
|
|
558
|
+
}));
|
|
559
|
+
// The permit guards only in-memory state, so acquiring it is
|
|
560
|
+
// interruptible: a cancelled caller is released instead of queueing
|
|
561
|
+
// uninterruptibly. The critical section itself stays uninterruptible.
|
|
562
|
+
const entry = yield* restore(mutation.take(1)).pipe(Effect.andThen(Effect.gen(function* () {
|
|
563
|
+
if (closed) {
|
|
564
|
+
return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }));
|
|
565
|
+
}
|
|
566
|
+
const ceiling = yield* current;
|
|
567
|
+
if (!allows(ceiling, request.capability)) {
|
|
568
|
+
return yield* Effect.fail(permissionDenied(request.capability, "outside capability ceiling"));
|
|
569
|
+
}
|
|
570
|
+
const decision = evaluate(rulesets(request.capability), request.capability);
|
|
571
|
+
if (decision === "allow") {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
if (decision === "deny") {
|
|
575
|
+
return yield* Effect.fail(permissionDenied(request.capability, "denied by permission policy"));
|
|
576
|
+
}
|
|
577
|
+
const tier = tierOf(request.capability, { workspaceRoot });
|
|
578
|
+
if (!attended) {
|
|
579
|
+
const requestId = yield* nextUnattendedRequestId;
|
|
580
|
+
return yield* Effect.fail(permissionRequired({
|
|
581
|
+
requestId,
|
|
582
|
+
runId,
|
|
583
|
+
capability: request.capability,
|
|
584
|
+
tier,
|
|
585
|
+
meta: request.meta
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
return yield* allocateRequest(request.capability, tier, request.meta, ceiling);
|
|
589
|
+
}).pipe(Effect.ensuring(mutation.release(1)))));
|
|
590
|
+
if (entry === undefined) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.ensuring(mutation.withPermit(Effect.sync(() => {
|
|
594
|
+
pending.delete(entry.requestId);
|
|
595
|
+
}))));
|
|
596
|
+
})));
|
|
597
|
+
const resolveCovered = Effect.suspend(() => Effect.forEach([...pending.entries()], ([requestId, entry]) => {
|
|
598
|
+
if (!allows(entry.ceiling, entry.capability) ||
|
|
599
|
+
evaluate(rulesets(entry.capability), entry.capability) !== "allow") {
|
|
600
|
+
return Effect.void;
|
|
601
|
+
}
|
|
602
|
+
return Deferred.succeed(entry.deferred, undefined).pipe(Effect.andThen(Effect.sync(() => {
|
|
603
|
+
pending.delete(requestId);
|
|
604
|
+
})));
|
|
605
|
+
}, { discard: true }).pipe(Effect.asVoid));
|
|
606
|
+
// Requests and envelopes with a journal write in flight. The mutation
|
|
607
|
+
// permit guards only in-memory state: a decision is planned under the
|
|
608
|
+
// permit, persisted without it, then activated under it again.
|
|
609
|
+
const resolving = new Set();
|
|
610
|
+
const granting = new Map();
|
|
611
|
+
// Planned rules occupy capacity until their write fails or they activate.
|
|
612
|
+
// Replies and envelopes share this count, guarded by the mutation permit.
|
|
613
|
+
let reservedRules = 0;
|
|
614
|
+
const planReply = (requestId, resolution, suppliedPattern) => Effect.gen(function* () {
|
|
615
|
+
if (closed) {
|
|
616
|
+
return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }));
|
|
617
|
+
}
|
|
618
|
+
if (!isResolution(resolution)) {
|
|
619
|
+
// A runtime-invalid resolution must fail the reply, not fall
|
|
620
|
+
// through the switch below: silently succeeding would strand
|
|
621
|
+
// the request's waiter on its Deferred forever. The request
|
|
622
|
+
// stays pending so the caller can still answer it.
|
|
623
|
+
return yield* Effect.fail(new GrantStoreError({
|
|
624
|
+
code: "invalid_resolution",
|
|
625
|
+
message: "unknown grant resolution"
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
const entry = pending.get(requestId);
|
|
629
|
+
// A request whose decision is already being persisted is decided:
|
|
630
|
+
// the losing reply fails fast instead of queueing behind the journal.
|
|
631
|
+
if (entry === undefined || resolving.has(requestId)) {
|
|
632
|
+
return yield* Effect.fail(new GrantStoreError({ code: "request_not_found" }));
|
|
633
|
+
}
|
|
634
|
+
const pattern = yield* attemptSnapshot("grant pattern", () => {
|
|
635
|
+
if (resolution === "run" || resolution === "remembered") {
|
|
636
|
+
if (suppliedPattern !== undefined)
|
|
637
|
+
return snapshotPattern(suppliedPattern);
|
|
638
|
+
const derived = patternFromCapability(entry.capability);
|
|
639
|
+
if (Option.isNone(derived)) {
|
|
640
|
+
throw invalid("the requested resource contains glob metacharacters; supply an explicit grant pattern or resolve once");
|
|
641
|
+
}
|
|
642
|
+
return snapshotPattern(derived.value);
|
|
643
|
+
}
|
|
644
|
+
return snapshotPattern(exactPattern(entry.capability));
|
|
645
|
+
});
|
|
646
|
+
switch (resolution) {
|
|
647
|
+
case "once": {
|
|
648
|
+
resolving.add(requestId);
|
|
649
|
+
return {
|
|
650
|
+
resolution,
|
|
651
|
+
entry,
|
|
652
|
+
event: new OnceGrant({
|
|
653
|
+
eventType: "flows.kernel.grant.once.v1",
|
|
654
|
+
requestId,
|
|
655
|
+
runId: runId ?? "",
|
|
656
|
+
...(planDigest === undefined ? {} : { planDigest }),
|
|
657
|
+
capability: entry.capability,
|
|
658
|
+
pattern,
|
|
659
|
+
scope: "once",
|
|
660
|
+
tier: entry.tier
|
|
661
|
+
})
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
case "run": {
|
|
665
|
+
if (planDigest === undefined) {
|
|
666
|
+
return yield* Effect.fail(new GrantStoreError({
|
|
667
|
+
code: "invalid_resolution",
|
|
668
|
+
message: "run grants require a plan digest"
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
if (!isValidGrantPattern(pattern, entry.capability, entry.tier, workspaceRoot)) {
|
|
672
|
+
return yield* Effect.fail(invalid("grant pattern exceeds the requested authority"));
|
|
673
|
+
}
|
|
674
|
+
if (configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
|
|
675
|
+
reservedRules >=
|
|
676
|
+
maximumRules) {
|
|
677
|
+
return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`));
|
|
678
|
+
}
|
|
679
|
+
resolving.add(requestId);
|
|
680
|
+
reservedRules += 1;
|
|
681
|
+
return {
|
|
682
|
+
resolution,
|
|
683
|
+
entry,
|
|
684
|
+
rule: snapshotRule(new Rule({ effect: "allow", pattern })),
|
|
685
|
+
event: new RunGrant({
|
|
686
|
+
eventType: "flows.kernel.grant.run.v2",
|
|
687
|
+
requestId,
|
|
688
|
+
runId: runId ?? "",
|
|
689
|
+
planDigest,
|
|
690
|
+
capability: entry.capability,
|
|
691
|
+
pattern,
|
|
692
|
+
ceiling: entry.ceiling.groups,
|
|
693
|
+
scope: "run",
|
|
694
|
+
tier: entry.tier
|
|
695
|
+
})
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
case "remembered": {
|
|
699
|
+
if (!isValidGrantPattern(pattern, entry.capability, entry.tier, workspaceRoot)) {
|
|
700
|
+
return yield* Effect.fail(invalid("grant pattern exceeds the requested authority"));
|
|
701
|
+
}
|
|
702
|
+
if (configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
|
|
703
|
+
reservedRules >=
|
|
704
|
+
maximumRules) {
|
|
705
|
+
return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`));
|
|
706
|
+
}
|
|
707
|
+
resolving.add(requestId);
|
|
708
|
+
reservedRules += 1;
|
|
709
|
+
return {
|
|
710
|
+
resolution,
|
|
711
|
+
entry,
|
|
712
|
+
rule: snapshotRule(new Rule({ effect: "allow", pattern })),
|
|
713
|
+
event: new RememberedGrant({
|
|
714
|
+
eventType: "flows.kernel.grant.remembered.v1",
|
|
715
|
+
requestId,
|
|
716
|
+
runId: runId ?? "",
|
|
717
|
+
...(planDigest === undefined ? {} : { planDigest }),
|
|
718
|
+
capability: entry.capability,
|
|
719
|
+
pattern,
|
|
720
|
+
scope: "remembered",
|
|
721
|
+
tier: entry.tier
|
|
722
|
+
})
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
case "deny": {
|
|
726
|
+
resolving.add(requestId);
|
|
727
|
+
return {
|
|
728
|
+
resolution,
|
|
729
|
+
entry,
|
|
730
|
+
event: new DeniedGrant({
|
|
731
|
+
eventType: "flows.kernel.grant.denied.v1",
|
|
732
|
+
requestId,
|
|
733
|
+
runId: runId ?? "",
|
|
734
|
+
...(planDigest === undefined ? {} : { planDigest }),
|
|
735
|
+
capability: entry.capability,
|
|
736
|
+
pattern,
|
|
737
|
+
scope: "once",
|
|
738
|
+
tier: entry.tier
|
|
739
|
+
})
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
const activateReply = (requestId, planned) => Effect.gen(function* () {
|
|
745
|
+
resolving.delete(requestId);
|
|
746
|
+
if (planned.resolution === "run" || planned.resolution === "remembered")
|
|
747
|
+
reservedRules -= 1;
|
|
748
|
+
if (closed) {
|
|
749
|
+
return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }));
|
|
750
|
+
}
|
|
751
|
+
const entry = pending.get(requestId);
|
|
752
|
+
switch (planned.resolution) {
|
|
753
|
+
case "once": {
|
|
754
|
+
if (entry === undefined) {
|
|
755
|
+
return yield* Effect.fail(new GrantStoreError({ code: "request_not_found" }));
|
|
756
|
+
}
|
|
757
|
+
yield* Deferred.succeed(entry.deferred, undefined);
|
|
758
|
+
pending.delete(requestId);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
case "deny": {
|
|
762
|
+
if (entry === undefined) {
|
|
763
|
+
return yield* Effect.fail(new GrantStoreError({ code: "request_not_found" }));
|
|
764
|
+
}
|
|
765
|
+
yield* Deferred.fail(entry.deferred, permissionDenied(entry.capability, "permission request denied"));
|
|
766
|
+
pending.delete(requestId);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
case "run": {
|
|
770
|
+
// The decision is already durable, so the rule activates even
|
|
771
|
+
// when the request's own waiter left while the journal wrote:
|
|
772
|
+
// replay installs it on the next open and the live store must
|
|
773
|
+
// not diverge from the journal. Capacity was validated before
|
|
774
|
+
// persistence.
|
|
775
|
+
runRules.push({ rule: planned.rule, ceiling: planned.entry.ceiling });
|
|
776
|
+
yield* resolveCovered;
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
case "remembered": {
|
|
780
|
+
rememberedRules.push(planned.rule);
|
|
781
|
+
yield* resolveCovered;
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
const reply = Effect.fn("GrantStore.reply")((requestId, resolution, suppliedPattern) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
787
|
+
const planned = yield* mutation.withPermit(planReply(requestId, resolution, suppliedPattern));
|
|
788
|
+
// Journal IO never holds the permit: only this reply waits on it,
|
|
789
|
+
// interruptibly, so the write deadline can fire and a cancelled
|
|
790
|
+
// reply is released. A failed write clears the in-flight mark so
|
|
791
|
+
// the request stays retryable.
|
|
792
|
+
yield* restore(persistEvent(planned.event)).pipe(Effect.onError(() => mutation.withPermit(Effect.sync(() => {
|
|
793
|
+
resolving.delete(requestId);
|
|
794
|
+
if (planned.resolution === "run" || planned.resolution === "remembered")
|
|
795
|
+
reservedRules -= 1;
|
|
796
|
+
}))));
|
|
797
|
+
yield* mutation.withPermit(activateReply(requestId, planned));
|
|
798
|
+
})));
|
|
799
|
+
const list = Effect.fn("GrantStore.list")(() => mutation.withPermit(Effect.sync(() =>
|
|
800
|
+
// Every entry's meta is already the frozen snapshot taken at check
|
|
801
|
+
// time; list copies the array and re-wraps identity without
|
|
802
|
+
// re-walking or re-serializing the metadata graph.
|
|
803
|
+
Object.freeze(Array.from(pending.values(), ({ requestId, capability, tier, meta }) => Object.freeze({
|
|
804
|
+
requestId,
|
|
805
|
+
capability: snapshotCapability(capability),
|
|
806
|
+
tier,
|
|
807
|
+
meta
|
|
808
|
+
}))))))();
|
|
809
|
+
const planEnvelope = (envelope) => Effect.gen(function* () {
|
|
810
|
+
if (closed) {
|
|
811
|
+
return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }));
|
|
812
|
+
}
|
|
813
|
+
const { patterns, scope } = envelope;
|
|
814
|
+
const signature = envelopeSignature(envelope.planDigest, scope, patterns);
|
|
815
|
+
if (grantedEnvelopes.has(signature))
|
|
816
|
+
return { _tag: "Durable" };
|
|
817
|
+
// A concurrent admission of the same envelope is already persisting;
|
|
818
|
+
// wait for its outcome rather than writing a duplicate record.
|
|
819
|
+
const inFlight = granting.get(signature);
|
|
820
|
+
if (inFlight !== undefined)
|
|
821
|
+
return { _tag: "Wait", completion: inFlight };
|
|
822
|
+
if (configuredRules.length + envelopeRules.length + runRules.length + rememberedRules.length +
|
|
823
|
+
reservedRules + patterns.length > maximumRules) {
|
|
824
|
+
return yield* Effect.fail(invalid(`rules exceed ${maximumRules} entries`));
|
|
825
|
+
}
|
|
826
|
+
if (grantedEnvelopes.size + granting.size >= maximumRules) {
|
|
827
|
+
return yield* Effect.fail(invalid(`grant envelopes exceed ${maximumRules} entries`));
|
|
828
|
+
}
|
|
829
|
+
const completion = yield* Deferred.make();
|
|
830
|
+
granting.set(signature, completion);
|
|
831
|
+
reservedRules += patterns.length;
|
|
832
|
+
return {
|
|
833
|
+
_tag: "Fresh",
|
|
834
|
+
signature,
|
|
835
|
+
scope,
|
|
836
|
+
patterns,
|
|
837
|
+
completion,
|
|
838
|
+
event: new EnvelopeGrant({
|
|
839
|
+
eventType: "flows.kernel.grant.envelope.v1",
|
|
840
|
+
runId: runId ?? "",
|
|
841
|
+
planDigest: envelope.planDigest,
|
|
842
|
+
patterns,
|
|
843
|
+
scope
|
|
844
|
+
})
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
const activateEnvelope = (planned) => Effect.gen(function* () {
|
|
848
|
+
granting.delete(planned.signature);
|
|
849
|
+
reservedRules -= planned.patterns.length;
|
|
850
|
+
if (closed) {
|
|
851
|
+
return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }));
|
|
852
|
+
}
|
|
853
|
+
// The decision is already durable, so its rules activate without
|
|
854
|
+
// re-checking capacity: replay installs them on the next open and the
|
|
855
|
+
// live store must not diverge from the journal.
|
|
856
|
+
grantedEnvelopes.add(planned.signature);
|
|
857
|
+
const destination = planned.scope === "remembered" ? rememberedRules : envelopeRules;
|
|
858
|
+
for (const pattern of planned.patterns) {
|
|
859
|
+
destination.push(snapshotRule(new Rule({ effect: "allow", pattern })));
|
|
860
|
+
}
|
|
861
|
+
yield* resolveCovered;
|
|
862
|
+
});
|
|
863
|
+
const grantEnvelope = Effect.fn("GrantStore.grantEnvelope")((input) => Effect.uninterruptibleMask((restore) => {
|
|
864
|
+
const admit = (prepared) => Effect.gen(function* () {
|
|
865
|
+
const planned = yield* mutation.withPermit(planEnvelope(prepared));
|
|
866
|
+
switch (planned._tag) {
|
|
867
|
+
case "Durable":
|
|
868
|
+
return;
|
|
869
|
+
case "Wait": {
|
|
870
|
+
// The identical envelope is mid-flight elsewhere; adopt its
|
|
871
|
+
// outcome. A failed write fails here too: re-planning would
|
|
872
|
+
// turn one journal outage into an unbounded loop of writes.
|
|
873
|
+
// An interrupted first admission decided nothing and can be
|
|
874
|
+
// retried. Check this caller's cancellation before re-planning
|
|
875
|
+
// so an interrupted duplicate cannot loop under the mask.
|
|
876
|
+
return yield* restore(Deferred.await(planned.completion)).pipe(Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause)
|
|
877
|
+
? restore(Effect.void).pipe(Effect.andThen(admit(prepared)))
|
|
878
|
+
: Effect.failCause(cause)));
|
|
879
|
+
}
|
|
880
|
+
case "Fresh": {
|
|
881
|
+
// Journal IO never holds the permit: only this admission
|
|
882
|
+
// waits on it, interruptibly, so the write deadline can fire.
|
|
883
|
+
// Concurrent duplicates are released with the same outcome.
|
|
884
|
+
yield* restore(persistEvent(planned.event)).pipe(Effect.onError((cause) => mutation.withPermit(Effect.gen(function* () {
|
|
885
|
+
granting.delete(planned.signature);
|
|
886
|
+
reservedRules -= planned.patterns.length;
|
|
887
|
+
yield* Deferred.failCause(planned.completion, cause);
|
|
888
|
+
}))));
|
|
889
|
+
yield* mutation.withPermit(activateEnvelope(planned)).pipe(Effect.tap(() => Deferred.succeed(planned.completion, undefined)), Effect.onError((cause) => Deferred.failCause(planned.completion, cause)));
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
return Effect.gen(function* () {
|
|
895
|
+
const prepared = yield* prepareEnvelope(input, planDigest, workspaceRoot);
|
|
896
|
+
if (prepared.patterns.length === 0)
|
|
897
|
+
return;
|
|
898
|
+
yield* admit(prepared);
|
|
899
|
+
});
|
|
900
|
+
}));
|
|
901
|
+
const policy = mutation.withPermit(Effect.gen(function* () {
|
|
902
|
+
if (closed)
|
|
903
|
+
return yield* Effect.fail(new GrantStoreError({ code: "store_closed" }));
|
|
904
|
+
const ceiling = yield* current;
|
|
905
|
+
const unbounded = (rules) => rules.map((rule) => Object.freeze({ rule: snapshotRule(rule), ceiling: Object.freeze({ groups: Object.freeze([]) }) }));
|
|
906
|
+
return Object.freeze({
|
|
907
|
+
ceiling,
|
|
908
|
+
groups: Object.freeze([
|
|
909
|
+
unbounded(configuredRules),
|
|
910
|
+
unbounded(envelopeRules),
|
|
911
|
+
runRules.map(({ rule, ceiling }) => Object.freeze({ rule: snapshotRule(rule), ceiling })),
|
|
912
|
+
unbounded(rememberedRules)
|
|
913
|
+
].map((group) => Object.freeze(group)))
|
|
914
|
+
});
|
|
915
|
+
}));
|
|
916
|
+
return GrantStore.of({ check, reply, list, grantEnvelope, policy });
|
|
917
|
+
});
|
|
918
|
+
/**
|
|
919
|
+
* Provides a scoped in-memory grant store.
|
|
920
|
+
*
|
|
921
|
+
* @category layers
|
|
922
|
+
* @since 1.0.0-rc.0
|
|
923
|
+
*/
|
|
924
|
+
export const layer = (options = {}) => Layer.effect(GrantStore, make(options));
|
|
925
|
+
/**
|
|
926
|
+
* An allow-all grant store.
|
|
927
|
+
*
|
|
928
|
+
* @category constructors
|
|
929
|
+
* @since 1.0.0-rc.0
|
|
930
|
+
*/
|
|
931
|
+
export const makeNoop = GrantStore.of({
|
|
932
|
+
policy: current.pipe(Effect.map((ceiling) => Object.freeze({
|
|
933
|
+
ceiling,
|
|
934
|
+
groups: Object.freeze([Object.freeze(["*", "net:private"].map((action) => Object.freeze({
|
|
935
|
+
rule: snapshotRule(new Rule({ effect: "allow", pattern: new CapabilityPattern({ action, resource: "**" }) })),
|
|
936
|
+
ceiling: Object.freeze({ groups: Object.freeze([]) })
|
|
937
|
+
})))])
|
|
938
|
+
}))),
|
|
939
|
+
check: Effect.fn("GrantStore.check")(() => Effect.void),
|
|
940
|
+
reply: Effect.fn("GrantStore.reply")(() => Effect.void),
|
|
941
|
+
list: Effect.fn("GrantStore.list")(() => Effect.succeed([]))(),
|
|
942
|
+
grantEnvelope: Effect.fn("GrantStore.grantEnvelope")(() => Effect.void)
|
|
943
|
+
});
|
|
944
|
+
/**
|
|
945
|
+
* Provides the allow-all grant store.
|
|
946
|
+
*
|
|
947
|
+
* @category layers
|
|
948
|
+
* @since 1.0.0-rc.0
|
|
949
|
+
*/
|
|
950
|
+
export const layerNoop = Layer.succeed(GrantStore)(makeNoop);
|
|
951
|
+
//# sourceMappingURL=GrantStore.js.map
|