@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.
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.9.23",
3
+ "version": "0.9.28",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",