@blamejs/core 0.9.23 → 0.9.28
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 +5 -0
- package/index.js +18 -1
- package/lib/agent-audit.js +45 -0
- package/lib/agent-event-bus.js +336 -0
- package/lib/agent-idempotency.js +2 -8
- package/lib/agent-orchestrator.js +2 -8
- package/lib/agent-posture-chain.js +208 -0
- package/lib/agent-saga.js +191 -0
- package/lib/agent-stream.js +237 -0
- package/lib/agent-tenant.js +308 -0
- package/lib/guard-event-bus-payload.js +217 -0
- package/lib/guard-event-bus-topic.js +150 -0
- package/lib/guard-posture-chain.js +201 -0
- package/lib/guard-saga-config.js +157 -0
- package/lib/guard-stream-args.js +166 -0
- package/lib/guard-tenant-id.js +138 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardPostureChain
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Posture Chain
|
|
6
|
+
* @order 442
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Validates cross-boundary posture-chain envelopes. The envelope
|
|
10
|
+
* carries the set of compliance regimes the call is operating
|
|
11
|
+
* under (`postureSet: ["hipaa", "pci-dss"]`), the hop trail
|
|
12
|
+
* (`chainTrail: ["api-gateway", "mail-agent", "audit"]`), per-hop
|
|
13
|
+
* timestamps, and hop count. Refuses:
|
|
14
|
+
*
|
|
15
|
+
* - oversized trail (default hop cap = 16; defends infinite
|
|
16
|
+
* recursion across agent delegation)
|
|
17
|
+
* - non-ASCII hop names (operator-greppable in audit logs)
|
|
18
|
+
* - duplicate hop in trail (recursion guard)
|
|
19
|
+
* - missing or non-monotonic enteredAt timestamps
|
|
20
|
+
* - posture set contains non-string entries OR duplicates
|
|
21
|
+
*
|
|
22
|
+
* @card
|
|
23
|
+
* Validates cross-boundary posture envelopes. Hop trail caps,
|
|
24
|
+
* ASCII-only hop names, monotonic timestamps, set-shape
|
|
25
|
+
* posture-regime entries.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var { defineClass } = require("./framework-error");
|
|
29
|
+
|
|
30
|
+
var GuardPostureChainError = defineClass("GuardPostureChainError", { alwaysPermanent: true });
|
|
31
|
+
|
|
32
|
+
var DEFAULT_PROFILE = "strict";
|
|
33
|
+
|
|
34
|
+
var PROFILES = Object.freeze({
|
|
35
|
+
strict: { maxHops: 16, maxHopBytes: 64, maxRegimes: 8 }, // allow:raw-byte-literal
|
|
36
|
+
balanced: { maxHops: 32, maxHopBytes: 128, maxRegimes: 16 }, // allow:raw-byte-literal
|
|
37
|
+
permissive: { maxHops: 128, maxHopBytes: 256, maxRegimes: 64 }, // allow:raw-byte-literal
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
41
|
+
hipaa: "strict",
|
|
42
|
+
"pci-dss": "strict",
|
|
43
|
+
gdpr: "strict",
|
|
44
|
+
soc2: "strict",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @primitive b.guardPostureChain.validate
|
|
49
|
+
* @signature b.guardPostureChain.validate(envelope, opts?)
|
|
50
|
+
* @since 0.9.28
|
|
51
|
+
* @status stable
|
|
52
|
+
* @related b.agent.postureChain.create
|
|
53
|
+
*
|
|
54
|
+
* Validate a posture-chain envelope. Returns the envelope on success;
|
|
55
|
+
* throws on refusal.
|
|
56
|
+
*
|
|
57
|
+
* @opts
|
|
58
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
59
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* b.guardPostureChain.validate({
|
|
63
|
+
* postureSet: ["hipaa"],
|
|
64
|
+
* chainTrail: ["api-gateway", "mail-agent"],
|
|
65
|
+
* enteredAt: [1700000000000, 1700000000100],
|
|
66
|
+
* hopCount: 2,
|
|
67
|
+
* });
|
|
68
|
+
*/
|
|
69
|
+
function validate(envelope, opts) {
|
|
70
|
+
opts = opts || {};
|
|
71
|
+
var profile = PROFILES[_resolveProfile(opts)];
|
|
72
|
+
if (!envelope || typeof envelope !== "object") {
|
|
73
|
+
throw new GuardPostureChainError("posture-chain/bad-input",
|
|
74
|
+
"guardPostureChain.validate: envelope required");
|
|
75
|
+
}
|
|
76
|
+
// postureSet — array of distinct ASCII regime names
|
|
77
|
+
if (!Array.isArray(envelope.postureSet)) {
|
|
78
|
+
throw new GuardPostureChainError("posture-chain/bad-posture-set",
|
|
79
|
+
"guardPostureChain.validate: postureSet must be an array");
|
|
80
|
+
}
|
|
81
|
+
if (envelope.postureSet.length > profile.maxRegimes) {
|
|
82
|
+
throw new GuardPostureChainError("posture-chain/too-many-regimes",
|
|
83
|
+
"guardPostureChain.validate: " + envelope.postureSet.length +
|
|
84
|
+
" regimes exceeds maxRegimes=" + profile.maxRegimes);
|
|
85
|
+
}
|
|
86
|
+
var regSeen = Object.create(null);
|
|
87
|
+
for (var r = 0; r < envelope.postureSet.length; r += 1) {
|
|
88
|
+
var regime = envelope.postureSet[r];
|
|
89
|
+
if (typeof regime !== "string" || regime.length === 0) {
|
|
90
|
+
throw new GuardPostureChainError("posture-chain/bad-regime",
|
|
91
|
+
"guardPostureChain.validate: postureSet[" + r + "] must be a non-empty string");
|
|
92
|
+
}
|
|
93
|
+
if (regSeen[regime]) {
|
|
94
|
+
throw new GuardPostureChainError("posture-chain/duplicate-regime",
|
|
95
|
+
"guardPostureChain.validate: duplicate regime '" + regime + "' in postureSet");
|
|
96
|
+
}
|
|
97
|
+
regSeen[regime] = true;
|
|
98
|
+
}
|
|
99
|
+
// chainTrail — bounded hop list
|
|
100
|
+
if (!Array.isArray(envelope.chainTrail)) {
|
|
101
|
+
throw new GuardPostureChainError("posture-chain/bad-trail",
|
|
102
|
+
"guardPostureChain.validate: chainTrail must be an array");
|
|
103
|
+
}
|
|
104
|
+
if (envelope.chainTrail.length > profile.maxHops) {
|
|
105
|
+
throw new GuardPostureChainError("posture-chain/hop-limit-exceeded",
|
|
106
|
+
"guardPostureChain.validate: " + envelope.chainTrail.length +
|
|
107
|
+
" hops exceeds maxHops=" + profile.maxHops);
|
|
108
|
+
}
|
|
109
|
+
var hopSeen = Object.create(null);
|
|
110
|
+
for (var h = 0; h < envelope.chainTrail.length; h += 1) {
|
|
111
|
+
var hop = envelope.chainTrail[h];
|
|
112
|
+
if (typeof hop !== "string" || hop.length === 0) {
|
|
113
|
+
throw new GuardPostureChainError("posture-chain/bad-hop",
|
|
114
|
+
"guardPostureChain.validate: chainTrail[" + h + "] must be a non-empty string");
|
|
115
|
+
}
|
|
116
|
+
if (Buffer.byteLength(hop, "utf8") > profile.maxHopBytes) {
|
|
117
|
+
throw new GuardPostureChainError("posture-chain/hop-name-too-long",
|
|
118
|
+
"guardPostureChain.validate: chainTrail[" + h + "] exceeds maxHopBytes=" + profile.maxHopBytes);
|
|
119
|
+
}
|
|
120
|
+
for (var hi = 0; hi < hop.length; hi += 1) {
|
|
121
|
+
var hc = hop.charCodeAt(hi);
|
|
122
|
+
if (hc > 0x7F) { // allow:raw-byte-literal — ASCII-only
|
|
123
|
+
throw new GuardPostureChainError("posture-chain/non-ascii-hop",
|
|
124
|
+
"guardPostureChain.validate: chainTrail[" + h + "] has non-ASCII codepoint");
|
|
125
|
+
}
|
|
126
|
+
if (hc < 0x20 || hc === 0x7F) { // allow:raw-byte-literal — C0/DEL
|
|
127
|
+
throw new GuardPostureChainError("posture-chain/bad-hop-char",
|
|
128
|
+
"guardPostureChain.validate: chainTrail[" + h + "] has forbidden char 0x" + hc.toString(16));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (hopSeen[hop]) {
|
|
132
|
+
throw new GuardPostureChainError("posture-chain/duplicate-hop",
|
|
133
|
+
"guardPostureChain.validate: duplicate hop '" + hop + "' in chainTrail");
|
|
134
|
+
}
|
|
135
|
+
hopSeen[hop] = true;
|
|
136
|
+
}
|
|
137
|
+
// enteredAt timestamps (optional but if present must match length + be monotonic)
|
|
138
|
+
if (typeof envelope.enteredAt !== "undefined") {
|
|
139
|
+
if (!Array.isArray(envelope.enteredAt)) {
|
|
140
|
+
throw new GuardPostureChainError("posture-chain/bad-entered-at",
|
|
141
|
+
"guardPostureChain.validate: enteredAt must be an array of timestamps");
|
|
142
|
+
}
|
|
143
|
+
if (envelope.enteredAt.length !== envelope.chainTrail.length) {
|
|
144
|
+
throw new GuardPostureChainError("posture-chain/entered-at-length-mismatch",
|
|
145
|
+
"guardPostureChain.validate: enteredAt length must equal chainTrail length");
|
|
146
|
+
}
|
|
147
|
+
var prevT = -Infinity;
|
|
148
|
+
for (var t = 0; t < envelope.enteredAt.length; t += 1) {
|
|
149
|
+
var ts = envelope.enteredAt[t];
|
|
150
|
+
if (typeof ts !== "number" || !isFinite(ts) || ts < 0) {
|
|
151
|
+
throw new GuardPostureChainError("posture-chain/bad-timestamp",
|
|
152
|
+
"guardPostureChain.validate: enteredAt[" + t + "] must be a finite non-negative number");
|
|
153
|
+
}
|
|
154
|
+
if (ts < prevT) {
|
|
155
|
+
throw new GuardPostureChainError("posture-chain/non-monotonic-timestamps",
|
|
156
|
+
"guardPostureChain.validate: enteredAt[" + t + "] < enteredAt[" + (t - 1) + "]");
|
|
157
|
+
}
|
|
158
|
+
prevT = ts;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return envelope;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @primitive b.guardPostureChain.compliancePosture
|
|
166
|
+
* @signature b.guardPostureChain.compliancePosture(posture)
|
|
167
|
+
* @since 0.9.28
|
|
168
|
+
* @status stable
|
|
169
|
+
*
|
|
170
|
+
* Return the effective profile for a given compliance posture name.
|
|
171
|
+
* Returns `null` for unknown posture names so operator typos surface
|
|
172
|
+
* here instead of silently falling through to the default profile.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* b.guardPostureChain.compliancePosture("hipaa"); // → "strict"
|
|
176
|
+
*/
|
|
177
|
+
function compliancePosture(posture) {
|
|
178
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _resolveProfile(opts) {
|
|
182
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
183
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
184
|
+
}
|
|
185
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
186
|
+
if (!PROFILES[p]) {
|
|
187
|
+
throw new GuardPostureChainError("posture-chain/bad-profile",
|
|
188
|
+
"guardPostureChain: unknown profile '" + p + "'");
|
|
189
|
+
}
|
|
190
|
+
return p;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
validate: validate,
|
|
195
|
+
compliancePosture: compliancePosture,
|
|
196
|
+
PROFILES: PROFILES,
|
|
197
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
198
|
+
GuardPostureChainError: GuardPostureChainError,
|
|
199
|
+
NAME: "postureChain",
|
|
200
|
+
KIND: "posture-chain",
|
|
201
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardSagaConfig
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Saga Config
|
|
6
|
+
* @order 441
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Saga-creation config validator. Refuses empty steps array,
|
|
10
|
+
* duplicate step names, non-ASCII saga name, non-async-function
|
|
11
|
+
* run/compensate fields, oversized step count.
|
|
12
|
+
*
|
|
13
|
+
* @card
|
|
14
|
+
* Validates `b.agent.saga.create()` opts. Step-list shape, name
|
|
15
|
+
* uniqueness, run/compensate function checks.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
var { defineClass } = require("./framework-error");
|
|
19
|
+
|
|
20
|
+
var GuardSagaConfigError = defineClass("GuardSagaConfigError", { alwaysPermanent: true });
|
|
21
|
+
|
|
22
|
+
var DEFAULT_PROFILE = "strict";
|
|
23
|
+
|
|
24
|
+
var PROFILES = Object.freeze({
|
|
25
|
+
strict: { maxSteps: 32, maxNameBytes: 64 }, // allow:raw-byte-literal
|
|
26
|
+
balanced: { maxSteps: 128, maxNameBytes: 128 }, // allow:raw-byte-literal
|
|
27
|
+
permissive: { maxSteps: 512, maxNameBytes: 256 }, // allow:raw-byte-literal
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
31
|
+
hipaa: "strict",
|
|
32
|
+
"pci-dss": "strict",
|
|
33
|
+
gdpr: "strict",
|
|
34
|
+
soc2: "strict",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @primitive b.guardSagaConfig.validate
|
|
39
|
+
* @signature b.guardSagaConfig.validate(config, opts?)
|
|
40
|
+
* @since 0.9.27
|
|
41
|
+
* @status stable
|
|
42
|
+
* @related b.agent.saga.create
|
|
43
|
+
*
|
|
44
|
+
* Validate saga config. Returns config on success; throws on refusal.
|
|
45
|
+
*
|
|
46
|
+
* @opts
|
|
47
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
48
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* b.guardSagaConfig.validate({
|
|
52
|
+
* name: "mail.send",
|
|
53
|
+
* steps: [
|
|
54
|
+
* { name: "sign", run: async () => {}, compensate: async () => {} },
|
|
55
|
+
* ],
|
|
56
|
+
* });
|
|
57
|
+
*/
|
|
58
|
+
function validate(config, opts) {
|
|
59
|
+
opts = opts || {};
|
|
60
|
+
var profile = PROFILES[_resolveProfile(opts)];
|
|
61
|
+
if (!config || typeof config !== "object") {
|
|
62
|
+
throw new GuardSagaConfigError("saga-config/bad-input",
|
|
63
|
+
"guardSagaConfig.validate: config required");
|
|
64
|
+
}
|
|
65
|
+
if (typeof config.name !== "string" || config.name.length === 0) {
|
|
66
|
+
throw new GuardSagaConfigError("saga-config/bad-name",
|
|
67
|
+
"guardSagaConfig.validate: name required");
|
|
68
|
+
}
|
|
69
|
+
if (Buffer.byteLength(config.name, "utf8") > profile.maxNameBytes) {
|
|
70
|
+
throw new GuardSagaConfigError("saga-config/name-too-long",
|
|
71
|
+
"guardSagaConfig.validate: name exceeds maxNameBytes=" + profile.maxNameBytes);
|
|
72
|
+
}
|
|
73
|
+
for (var i = 0; i < config.name.length; i += 1) {
|
|
74
|
+
var c = config.name.charCodeAt(i);
|
|
75
|
+
if (c > 0x7F) { // allow:raw-byte-literal — ASCII-only
|
|
76
|
+
throw new GuardSagaConfigError("saga-config/non-ascii-name",
|
|
77
|
+
"guardSagaConfig.validate: name has non-ASCII codepoint at offset " + i);
|
|
78
|
+
}
|
|
79
|
+
if (c < 0x20 || c === 0x7F) { // allow:raw-byte-literal — C0/DEL
|
|
80
|
+
throw new GuardSagaConfigError("saga-config/bad-name-char",
|
|
81
|
+
"guardSagaConfig.validate: name has forbidden char 0x" + c.toString(16));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(config.steps) || config.steps.length === 0) {
|
|
85
|
+
throw new GuardSagaConfigError("saga-config/no-steps",
|
|
86
|
+
"guardSagaConfig.validate: steps must be a non-empty array");
|
|
87
|
+
}
|
|
88
|
+
if (config.steps.length > profile.maxSteps) {
|
|
89
|
+
throw new GuardSagaConfigError("saga-config/too-many-steps",
|
|
90
|
+
"guardSagaConfig.validate: " + config.steps.length + " steps exceeds " + profile.maxSteps);
|
|
91
|
+
}
|
|
92
|
+
var seenNames = Object.create(null);
|
|
93
|
+
for (var s = 0; s < config.steps.length; s += 1) {
|
|
94
|
+
var step = config.steps[s];
|
|
95
|
+
if (!step || typeof step !== "object") {
|
|
96
|
+
throw new GuardSagaConfigError("saga-config/bad-step",
|
|
97
|
+
"guardSagaConfig.validate: steps[" + s + "] must be an object");
|
|
98
|
+
}
|
|
99
|
+
if (typeof step.name !== "string" || step.name.length === 0) {
|
|
100
|
+
throw new GuardSagaConfigError("saga-config/bad-step-name",
|
|
101
|
+
"guardSagaConfig.validate: steps[" + s + "].name required");
|
|
102
|
+
}
|
|
103
|
+
if (seenNames[step.name]) {
|
|
104
|
+
throw new GuardSagaConfigError("saga-config/duplicate-step-name",
|
|
105
|
+
"guardSagaConfig.validate: duplicate step name '" + step.name + "'");
|
|
106
|
+
}
|
|
107
|
+
seenNames[step.name] = true;
|
|
108
|
+
if (typeof step.run !== "function") {
|
|
109
|
+
throw new GuardSagaConfigError("saga-config/bad-step-run",
|
|
110
|
+
"guardSagaConfig.validate: steps[" + s + "].run must be a function");
|
|
111
|
+
}
|
|
112
|
+
if (typeof step.compensate !== "undefined" && typeof step.compensate !== "function") {
|
|
113
|
+
throw new GuardSagaConfigError("saga-config/bad-step-compensate",
|
|
114
|
+
"guardSagaConfig.validate: steps[" + s + "].compensate must be a function (or omitted)");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return config;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @primitive b.guardSagaConfig.compliancePosture
|
|
122
|
+
* @signature b.guardSagaConfig.compliancePosture(posture)
|
|
123
|
+
* @since 0.9.27
|
|
124
|
+
* @status stable
|
|
125
|
+
*
|
|
126
|
+
* Return the effective profile for a given compliance posture name.
|
|
127
|
+
* Returns `null` for unknown posture names so operator typos surface
|
|
128
|
+
* here instead of silently falling through to the default profile.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* b.guardSagaConfig.compliancePosture("hipaa"); // → "strict"
|
|
132
|
+
*/
|
|
133
|
+
function compliancePosture(posture) {
|
|
134
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _resolveProfile(opts) {
|
|
138
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
139
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
140
|
+
}
|
|
141
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
142
|
+
if (!PROFILES[p]) {
|
|
143
|
+
throw new GuardSagaConfigError("saga-config/bad-profile",
|
|
144
|
+
"guardSagaConfig: unknown profile '" + p + "'");
|
|
145
|
+
}
|
|
146
|
+
return p;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
validate: validate,
|
|
151
|
+
compliancePosture: compliancePosture,
|
|
152
|
+
PROFILES: PROFILES,
|
|
153
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
154
|
+
GuardSagaConfigError: GuardSagaConfigError,
|
|
155
|
+
NAME: "sagaConfig",
|
|
156
|
+
KIND: "saga-config",
|
|
157
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardStreamArgs
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Stream Args
|
|
6
|
+
* @order 437
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Validates `b.agent.stream.create` opts (and the operator-supplied
|
|
10
|
+
* stream args). Refuses non-positive batch sizes, non-integer batch
|
|
11
|
+
* sizes (silent shard-style routing drift class — same shape Codex
|
|
12
|
+
* caught on v0.9.21), oversized batch sizes (back-pressure becomes
|
|
13
|
+
* meaningless), and structured-clone-unsafe filter shapes (functions
|
|
14
|
+
* / regex / Buffer in the cursor opts — same shape `b.guardMailQuery`
|
|
15
|
+
* refuses).
|
|
16
|
+
*
|
|
17
|
+
* @card
|
|
18
|
+
* Validates `b.agent.stream.create` opts + cursor args. Integer-only
|
|
19
|
+
* batchSize, structured-clone-safe filter shapes, sensible caps.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
var { defineClass } = require("./framework-error");
|
|
23
|
+
|
|
24
|
+
var GuardStreamArgsError = defineClass("GuardStreamArgsError", { alwaysPermanent: true });
|
|
25
|
+
|
|
26
|
+
var DEFAULT_PROFILE = "strict";
|
|
27
|
+
|
|
28
|
+
var PROFILES = Object.freeze({
|
|
29
|
+
strict: { maxBatchSize: 1024, minBatchSize: 1, maxOpenStreams: 4 }, // allow:raw-byte-literal
|
|
30
|
+
balanced: { maxBatchSize: 4096, minBatchSize: 1, maxOpenStreams: 16 }, // allow:raw-byte-literal
|
|
31
|
+
permissive: { maxBatchSize: 16384, minBatchSize: 1, maxOpenStreams: 64 }, // allow:raw-byte-literal
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
35
|
+
hipaa: "strict",
|
|
36
|
+
"pci-dss": "strict",
|
|
37
|
+
gdpr: "strict",
|
|
38
|
+
soc2: "strict",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @primitive b.guardStreamArgs.validate
|
|
43
|
+
* @signature b.guardStreamArgs.validate(args, opts?)
|
|
44
|
+
* @since 0.9.24
|
|
45
|
+
* @status stable
|
|
46
|
+
* @related b.agent.stream.create
|
|
47
|
+
*
|
|
48
|
+
* Validate `b.agent.stream.create` opts shape. Returns the input on
|
|
49
|
+
* success; throws `GuardStreamArgsError` on refusal.
|
|
50
|
+
*
|
|
51
|
+
* @opts
|
|
52
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
53
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* b.guardStreamArgs.validate({
|
|
57
|
+
* batchSize: 256,
|
|
58
|
+
* kind: "search",
|
|
59
|
+
* });
|
|
60
|
+
*/
|
|
61
|
+
function validate(args, opts) {
|
|
62
|
+
opts = opts || {};
|
|
63
|
+
var profile = PROFILES[_resolveProfile(opts)];
|
|
64
|
+
if (!args || typeof args !== "object") {
|
|
65
|
+
throw new GuardStreamArgsError("stream-args/bad-input",
|
|
66
|
+
"guardStreamArgs.validate: args required");
|
|
67
|
+
}
|
|
68
|
+
if (typeof args.batchSize !== "undefined") {
|
|
69
|
+
if (!Number.isInteger(args.batchSize)) {
|
|
70
|
+
throw new GuardStreamArgsError("stream-args/bad-batch-size",
|
|
71
|
+
"guardStreamArgs.validate: batchSize must be an integer");
|
|
72
|
+
}
|
|
73
|
+
if (args.batchSize < profile.minBatchSize || args.batchSize > profile.maxBatchSize) {
|
|
74
|
+
throw new GuardStreamArgsError("stream-args/batch-size-out-of-range",
|
|
75
|
+
"guardStreamArgs.validate: batchSize " + args.batchSize +
|
|
76
|
+
" not in [" + profile.minBatchSize + ", " + profile.maxBatchSize + "]");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (typeof args.kind !== "undefined") {
|
|
80
|
+
if (typeof args.kind !== "string" || args.kind.length === 0) {
|
|
81
|
+
throw new GuardStreamArgsError("stream-args/bad-kind",
|
|
82
|
+
"guardStreamArgs.validate: kind must be a non-empty string");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Cursor opts can't carry function / regex / Buffer — they must
|
|
86
|
+
// cross the structured-clone boundary into a worker thread.
|
|
87
|
+
if (typeof args.cursorOpts !== "undefined") {
|
|
88
|
+
_checkCursorOpts(args.cursorOpts);
|
|
89
|
+
}
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @primitive b.guardStreamArgs.compliancePosture
|
|
95
|
+
* @signature b.guardStreamArgs.compliancePosture(posture)
|
|
96
|
+
* @since 0.9.24
|
|
97
|
+
* @status stable
|
|
98
|
+
*
|
|
99
|
+
* Return the effective profile for a given compliance posture name.
|
|
100
|
+
* Returns `null` for unknown posture names so operator typos surface
|
|
101
|
+
* here instead of silently falling through to the default profile.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* b.guardStreamArgs.compliancePosture("hipaa"); // → "strict"
|
|
105
|
+
*/
|
|
106
|
+
function compliancePosture(posture) {
|
|
107
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _checkCursorOpts(cursorOpts, depth) {
|
|
111
|
+
depth = depth || 0;
|
|
112
|
+
if (depth > 8) { // allow:raw-byte-literal — recursion depth cap
|
|
113
|
+
throw new GuardStreamArgsError("stream-args/cursor-opts-too-deep",
|
|
114
|
+
"guardStreamArgs.validate: cursorOpts nesting depth exceeds 8");
|
|
115
|
+
}
|
|
116
|
+
// Function check FIRST — `typeof function === "function"` not
|
|
117
|
+
// "object", so a function value would silently skip the non-object
|
|
118
|
+
// early-return below.
|
|
119
|
+
if (typeof cursorOpts === "function") {
|
|
120
|
+
throw new GuardStreamArgsError("stream-args/function-not-allowed",
|
|
121
|
+
"guardStreamArgs.validate: functions refused in cursorOpts (structured-clone-unsafe)");
|
|
122
|
+
}
|
|
123
|
+
if (cursorOpts === null || typeof cursorOpts !== "object") return;
|
|
124
|
+
if (cursorOpts instanceof RegExp) {
|
|
125
|
+
throw new GuardStreamArgsError("stream-args/regex-not-allowed",
|
|
126
|
+
"guardStreamArgs.validate: RegExp refused in cursorOpts");
|
|
127
|
+
}
|
|
128
|
+
if (Buffer.isBuffer(cursorOpts)) {
|
|
129
|
+
throw new GuardStreamArgsError("stream-args/buffer-not-allowed",
|
|
130
|
+
"guardStreamArgs.validate: Buffer refused in cursorOpts");
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(cursorOpts)) {
|
|
133
|
+
for (var i = 0; i < cursorOpts.length; i += 1) _checkCursorOpts(cursorOpts[i], depth + 1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
var keys = Object.keys(cursorOpts);
|
|
137
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
138
|
+
if (keys[k] === "__proto__" || keys[k] === "constructor" || keys[k] === "prototype") {
|
|
139
|
+
throw new GuardStreamArgsError("stream-args/proto-key",
|
|
140
|
+
"guardStreamArgs.validate: forbidden key '" + keys[k] + "' in cursorOpts");
|
|
141
|
+
}
|
|
142
|
+
_checkCursorOpts(cursorOpts[keys[k]], depth + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _resolveProfile(opts) {
|
|
147
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
148
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
149
|
+
}
|
|
150
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
151
|
+
if (!PROFILES[p]) {
|
|
152
|
+
throw new GuardStreamArgsError("stream-args/bad-profile",
|
|
153
|
+
"guardStreamArgs: unknown profile '" + p + "'");
|
|
154
|
+
}
|
|
155
|
+
return p;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
validate: validate,
|
|
160
|
+
compliancePosture: compliancePosture,
|
|
161
|
+
PROFILES: PROFILES,
|
|
162
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
163
|
+
GuardStreamArgsError: GuardStreamArgsError,
|
|
164
|
+
NAME: "streamArgs",
|
|
165
|
+
KIND: "stream-args",
|
|
166
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardTenantId
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Tenant Id
|
|
6
|
+
* @order 440
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Tenant-id shape validator. Tenant ids surface in audit log lines,
|
|
10
|
+
* sealed registry rows, derived-key context labels, and routing
|
|
11
|
+
* keys — they have to be ASCII-greppable across the whole framework
|
|
12
|
+
* stack. Refuses:
|
|
13
|
+
*
|
|
14
|
+
* - non-ASCII (NFC + ASCII-only)
|
|
15
|
+
* - path-traversal shapes (`..` / `/` / `\` / NUL / C0 / DEL)
|
|
16
|
+
* - oversized (default 64 bytes)
|
|
17
|
+
* - reserved `ROOT` / `FRAMEWORK` / `*` / empty
|
|
18
|
+
* - leading `.` (hidden-folder shape)
|
|
19
|
+
*
|
|
20
|
+
* @card
|
|
21
|
+
* Validates tenant-id strings. ASCII-only, bounded, no path-
|
|
22
|
+
* traversal, no reserved names.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
var { defineClass } = require("./framework-error");
|
|
26
|
+
|
|
27
|
+
var GuardTenantIdError = defineClass("GuardTenantIdError", { alwaysPermanent: true });
|
|
28
|
+
|
|
29
|
+
var DEFAULT_PROFILE = "strict";
|
|
30
|
+
|
|
31
|
+
var PROFILES = Object.freeze({
|
|
32
|
+
strict: { maxBytes: 64 }, // allow:raw-byte-literal
|
|
33
|
+
balanced: { maxBytes: 128 }, // allow:raw-byte-literal
|
|
34
|
+
permissive: { maxBytes: 512 }, // allow:raw-byte-literal
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
38
|
+
hipaa: "strict",
|
|
39
|
+
"pci-dss": "strict",
|
|
40
|
+
gdpr: "strict",
|
|
41
|
+
soc2: "strict",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
var RESERVED = Object.freeze({ "ROOT": true, "FRAMEWORK": true, "*": true });
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @primitive b.guardTenantId.validate
|
|
48
|
+
* @signature b.guardTenantId.validate(tenantId, opts?)
|
|
49
|
+
* @since 0.9.26
|
|
50
|
+
* @status stable
|
|
51
|
+
* @related b.agent.tenant.create
|
|
52
|
+
*
|
|
53
|
+
* Validate a tenant-id string. Returns the id on success; throws
|
|
54
|
+
* `GuardTenantIdError` on refusal.
|
|
55
|
+
*
|
|
56
|
+
* @opts
|
|
57
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
58
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* b.guardTenantId.validate("acme-clinic");
|
|
62
|
+
*/
|
|
63
|
+
function validate(tenantId, opts) {
|
|
64
|
+
opts = opts || {};
|
|
65
|
+
var profile = PROFILES[_resolveProfile(opts)];
|
|
66
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) {
|
|
67
|
+
throw new GuardTenantIdError("tenant-id/bad-input",
|
|
68
|
+
"guardTenantId.validate: tenantId must be a non-empty string");
|
|
69
|
+
}
|
|
70
|
+
if (Buffer.byteLength(tenantId, "utf8") > profile.maxBytes) {
|
|
71
|
+
throw new GuardTenantIdError("tenant-id/oversize",
|
|
72
|
+
"guardTenantId.validate: tenantId exceeds maxBytes=" + profile.maxBytes);
|
|
73
|
+
}
|
|
74
|
+
if (RESERVED[tenantId]) {
|
|
75
|
+
throw new GuardTenantIdError("tenant-id/reserved",
|
|
76
|
+
"guardTenantId.validate: tenantId '" + tenantId + "' is framework-reserved");
|
|
77
|
+
}
|
|
78
|
+
if (tenantId.charAt(0) === ".") {
|
|
79
|
+
throw new GuardTenantIdError("tenant-id/hidden",
|
|
80
|
+
"guardTenantId.validate: tenantId cannot start with '.'");
|
|
81
|
+
}
|
|
82
|
+
if (tenantId.indexOf("..") >= 0) {
|
|
83
|
+
throw new GuardTenantIdError("tenant-id/path-traversal",
|
|
84
|
+
"guardTenantId.validate: tenantId contains '..'");
|
|
85
|
+
}
|
|
86
|
+
for (var i = 0; i < tenantId.length; i += 1) {
|
|
87
|
+
var c = tenantId.charCodeAt(i);
|
|
88
|
+
if (c > 0x7F) { // allow:raw-byte-literal — ASCII-only cap
|
|
89
|
+
throw new GuardTenantIdError("tenant-id/non-ascii",
|
|
90
|
+
"guardTenantId.validate: non-ASCII codepoint at offset " + i);
|
|
91
|
+
}
|
|
92
|
+
if (c < 0x20 || c === 0x7F || c === 0x2F || c === 0x5C) { // allow:raw-byte-literal — C0/DEL/slash/backslash
|
|
93
|
+
throw new GuardTenantIdError("tenant-id/bad-char",
|
|
94
|
+
"guardTenantId.validate: forbidden char 0x" + c.toString(16) + " at offset " + i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return tenantId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @primitive b.guardTenantId.compliancePosture
|
|
102
|
+
* @signature b.guardTenantId.compliancePosture(posture)
|
|
103
|
+
* @since 0.9.26
|
|
104
|
+
* @status stable
|
|
105
|
+
*
|
|
106
|
+
* Return the effective profile for a given compliance posture name.
|
|
107
|
+
* Returns `null` for unknown posture names so operator typos surface
|
|
108
|
+
* here instead of silently falling through to the default profile.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* b.guardTenantId.compliancePosture("hipaa"); // → "strict"
|
|
112
|
+
*/
|
|
113
|
+
function compliancePosture(posture) {
|
|
114
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _resolveProfile(opts) {
|
|
118
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
119
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
120
|
+
}
|
|
121
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
122
|
+
if (!PROFILES[p]) {
|
|
123
|
+
throw new GuardTenantIdError("tenant-id/bad-profile",
|
|
124
|
+
"guardTenantId: unknown profile '" + p + "'");
|
|
125
|
+
}
|
|
126
|
+
return p;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
validate: validate,
|
|
131
|
+
compliancePosture: compliancePosture,
|
|
132
|
+
PROFILES: PROFILES,
|
|
133
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
134
|
+
RESERVED: RESERVED,
|
|
135
|
+
GuardTenantIdError: GuardTenantIdError,
|
|
136
|
+
NAME: "tenantId",
|
|
137
|
+
KIND: "tenant-id",
|
|
138
|
+
};
|