@frontmcp/utils 1.10.16 → 1.11.0-rc.2
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/README.md +1 -0
- package/browser/async-context/browser-async-context.mjs +177 -0
- package/browser/async-context/index.mjs +15 -0
- package/browser/async-context/overlap-error.mjs +10 -0
- package/browser/class-brand/class-brand.mjs +27 -0
- package/browser/class-brand/index.mjs +6 -0
- package/browser/content/content.mjs +151 -0
- package/browser/content/index.mjs +8 -0
- package/browser/crypto/browser.mjs +103 -0
- package/browser/crypto/encrypted-blob.mjs +119 -0
- package/browser/crypto/hmac-signing.mjs +70 -0
- package/browser/crypto/index.mjs +281 -0
- package/browser/crypto/jwt-alg.mjs +40 -0
- package/browser/crypto/key-persistence/factory.mjs +134 -0
- package/browser/crypto/key-persistence/index.mjs +26 -0
- package/browser/crypto/key-persistence/key-persistence.mjs +185 -0
- package/browser/crypto/key-persistence/schemas.mjs +82 -0
- package/browser/crypto/pkce/index.mjs +25 -0
- package/browser/crypto/pkce/pkce.mjs +59 -0
- package/browser/crypto/runtime.mjs +17 -0
- package/browser/crypto/secret-persistence/index.mjs +29 -0
- package/browser/crypto/secret-persistence/persistence.mjs +181 -0
- package/browser/crypto/secret-persistence/schema.mjs +40 -0
- package/browser/crypto/web-rsa.mjs +36 -0
- package/browser/env/browser-env.mjs +56 -0
- package/browser/env/browser-runtime-context.mjs +85 -0
- package/browser/env/build-target.mjs +25 -0
- package/browser/env/edge-scope.mjs +17 -0
- package/browser/env/index.mjs +47 -0
- package/browser/env/provider.mjs +46 -0
- package/browser/escape/escape.mjs +43 -0
- package/browser/escape/index.mjs +9 -0
- package/browser/event-emitter/browser-event-emitter.mjs +49 -0
- package/browser/event-emitter/index.mjs +5 -0
- package/browser/fs/fs.mjs +227 -0
- package/browser/fs/index.mjs +65 -0
- package/browser/http/cookie.mjs +85 -0
- package/browser/http/http.mjs +18 -0
- package/browser/http/index.mjs +11 -0
- package/browser/index.mjs +526 -0
- package/browser/llm/index.mjs +5 -0
- package/browser/llm/llm-tool-handlers.mjs +55 -0
- package/browser/machine-id/index.mjs +7 -0
- package/browser/machine-id/machine-id.mjs +108 -0
- package/browser/naming/index.mjs +10 -0
- package/browser/naming/naming.mjs +68 -0
- package/browser/net/index.mjs +10 -0
- package/browser/net/ip-address.mjs +128 -0
- package/browser/net/redirect.mjs +7 -0
- package/browser/path/browser-path.mjs +47 -0
- package/browser/path/index.mjs +24 -0
- package/browser/path/path.mjs +13 -0
- package/browser/process/child-process.mjs +23 -0
- package/browser/process/index.mjs +15 -0
- package/browser/process/owned-process.mjs +175 -0
- package/browser/process/process-tree.mjs +149 -0
- package/browser/process/process-watch.mjs +48 -0
- package/browser/redaction/index.mjs +6 -0
- package/browser/redaction/redaction.mjs +74 -0
- package/browser/regex/index.mjs +3 -0
- package/browser/regex/patterns.mjs +167 -0
- package/browser/regex/safe-regex.mjs +93 -0
- package/browser/serialization/index.mjs +5 -0
- package/browser/serialization/serialization.mjs +22 -0
- package/browser/stdio-bridge/errors.mjs +27 -0
- package/browser/stdio-bridge/index.mjs +24 -0
- package/browser/stdio-bridge/list-changed.mjs +21 -0
- package/browser/stdio-bridge/state-machine.mjs +211 -0
- package/browser/stdio-bridge/stdio-framer.mjs +91 -0
- package/browser/stdio-bridge/upstream-client.mjs +189 -0
- package/browser/storage/adapters/base.mjs +139 -0
- package/browser/storage/adapters/cloudflare-kv.mjs +149 -0
- package/browser/storage/adapters/filesystem.mjs +279 -0
- package/browser/storage/adapters/index.mjs +23 -0
- package/browser/storage/adapters/indexeddb.mjs +198 -0
- package/browser/storage/adapters/localstorage.mjs +222 -0
- package/browser/storage/adapters/memory.mjs +350 -0
- package/browser/storage/adapters/redis.mjs +497 -0
- package/browser/storage/adapters/upstash.mjs +299 -0
- package/browser/storage/adapters/vercel-kv.mjs +226 -0
- package/browser/storage/change-feed.mjs +189 -0
- package/browser/storage/encrypted-typed-storage.mjs +329 -0
- package/browser/storage/errors.mjs +92 -0
- package/browser/storage/factory.mjs +155 -0
- package/browser/storage/index.mjs +96 -0
- package/browser/storage/namespace.mjs +186 -0
- package/browser/storage/redis-error-listener.mjs +32 -0
- package/browser/storage/redis-url.mjs +66 -0
- package/browser/storage/typed-storage.mjs +171 -0
- package/browser/storage/utils/compare-and-delete.mjs +9 -0
- package/browser/storage/utils/index.mjs +24 -0
- package/browser/storage/utils/pattern.mjs +68 -0
- package/browser/storage/utils/ttl.mjs +48 -0
- package/browser/uri/index.mjs +19 -0
- package/browser/uri/uri-template.mjs +86 -0
- package/browser/uri/uri-validation.mjs +18 -0
- package/crypto/browser.d.ts +1 -13
- package/crypto/browser.js +1 -1
- package/crypto/node.js +1 -1
- package/crypto/web-rsa.d.ts +13 -0
- package/env/browser-runtime-context.js +1 -1
- package/env/runtime-context.js +1 -1
- package/esm/crypto/browser.mjs +1 -1
- package/esm/crypto/node.mjs +1 -1
- package/esm/env/browser-runtime-context.mjs +1 -1
- package/esm/env/runtime-context.mjs +1 -1
- package/esm/index.mjs +1 -1
- package/fs/fs.d.ts +20 -4
- package/fs/index.d.ts +1 -1
- package/index.d.ts +9 -3
- package/index.js +1 -1
- package/package.json +24 -8
- package/process/child-process.d.ts +2 -0
- package/process/index.d.ts +6 -0
- package/process/owned-process.d.ts +59 -0
- package/process/process-tree.d.ts +78 -0
- package/process/process-watch.d.ts +41 -0
- package/redaction/index.d.ts +2 -0
- package/redaction/redaction.d.ts +24 -0
- package/stdio-bridge/errors.d.ts +23 -0
- package/stdio-bridge/index.d.ts +9 -0
- package/stdio-bridge/list-changed.d.ts +3 -0
- package/stdio-bridge/state-machine.d.ts +61 -0
- package/stdio-bridge/stdio-framer.d.ts +49 -0
- package/stdio-bridge/types.d.ts +10 -0
- package/stdio-bridge/upstream-client.d.ts +66 -0
- package/storage/adapters/base.d.ts +2 -0
- package/storage/adapters/memory.d.ts +1 -0
- package/storage/adapters/redis.d.ts +3 -0
- package/storage/change-feed.d.ts +47 -0
- package/storage/factory.d.ts +6 -0
- package/storage/index.d.ts +3 -1
- package/storage/namespace.d.ts +2 -0
- package/storage/types.d.ts +46 -1
- package/storage/utils/compare-and-delete.d.ts +2 -0
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ await storage.get('ticket:T-1'); // 'open'
|
|
|
51
51
|
| Encryption, keys | `encryptValue`, `decryptValue`, `signData`, `verifyData`, `KeyPersistence`, `createKeyPersistence` |
|
|
52
52
|
| Storage | `createStorage`, `createMemoryStorage`, `TypedStorage`, `EncryptedTypedStorage`, and adapters for memory, Redis, Vercel KV, Upstash and the file system |
|
|
53
53
|
| File system | `readFile`, `writeFile`, `mkdir`, `stat`, `fileExists`, `readJSON`, `writeJSON`, `ensureDir`, `isDirEmpty`, `runCmd` and more. Node.js only; `fs` loads on first use. |
|
|
54
|
+
| Processes | `spawnOwned`, `reapOrphans`, `watchParent`, `watchPath`, `stopProcessTree`, `signalProcessTree`. Node.js only; `child_process` loads on first use. |
|
|
54
55
|
| Environment | `getEnv`, `isProduction`, `isDevelopment`, `isEdgeRuntime`, `isServerless`, `getRuntimeContext`, `isNode`, `isBrowser` |
|
|
55
56
|
| Context, events | `AsyncLocalStorage`, `EventEmitter`, both with browser builds |
|
|
56
57
|
| Safe regex | `isPatternSafe`, `createSafeRegExp`, `safeTest`, `safeMatch`, `safeReplace`, against ReDoS (regular expression denial of service) |
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// libs/utils/src/async-context/browser-async-context.ts
|
|
2
|
+
import { AsyncContextOverlapError } from "./overlap-error.mjs";
|
|
3
|
+
import { AsyncContextOverlapError as AsyncContextOverlapError2 } from "./overlap-error.mjs";
|
|
4
|
+
var nativeAsyncContext = (() => {
|
|
5
|
+
const candidate = globalThis.AsyncContext;
|
|
6
|
+
return typeof candidate?.Variable === "function" ? candidate : void 0;
|
|
7
|
+
})();
|
|
8
|
+
function getAsyncContextMode() {
|
|
9
|
+
return nativeAsyncContext ? "native" : "serialized";
|
|
10
|
+
}
|
|
11
|
+
var SLOW_TURN_WARNING_MS = 1e4;
|
|
12
|
+
var RequestSlot = class {
|
|
13
|
+
/** Frames of every storage that are still running in this request. */
|
|
14
|
+
live = 0;
|
|
15
|
+
/** Storages whose runs overlapped in this request; they refuse to answer until they unwind. */
|
|
16
|
+
tainted = /* @__PURE__ */ new Set();
|
|
17
|
+
idleWaiters = [];
|
|
18
|
+
enter() {
|
|
19
|
+
this.live++;
|
|
20
|
+
}
|
|
21
|
+
leave() {
|
|
22
|
+
this.live--;
|
|
23
|
+
if (this.live === 0) {
|
|
24
|
+
for (const resolve of this.idleWaiters.splice(0)) resolve();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Resolves once no frame of this request is running any more. */
|
|
28
|
+
idle() {
|
|
29
|
+
return this.live === 0 ? Promise.resolve() : new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ROOT = new RequestSlot();
|
|
33
|
+
var active = ROOT;
|
|
34
|
+
var owner;
|
|
35
|
+
var turnQueue = [];
|
|
36
|
+
var slowTurnWarned = false;
|
|
37
|
+
var overlapReported = false;
|
|
38
|
+
function takeTurn(slot) {
|
|
39
|
+
if (!owner) {
|
|
40
|
+
owner = slot;
|
|
41
|
+
active = slot;
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
if (slowTurnWarned) return;
|
|
47
|
+
slowTurnWarned = true;
|
|
48
|
+
console.warn(
|
|
49
|
+
"[frontmcp] A request has waited more than 10s for another one to finish. This runtime has no AsyncContext, so FrontMCP runs requests one at a time to keep their contexts apart. A tool that calls its own server through a client (instead of this.scope flows) waits for itself."
|
|
50
|
+
);
|
|
51
|
+
}, SLOW_TURN_WARNING_MS);
|
|
52
|
+
timer.unref?.();
|
|
53
|
+
turnQueue.push({
|
|
54
|
+
slot,
|
|
55
|
+
start: () => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function passTurn(slot) {
|
|
63
|
+
if (owner !== slot) return;
|
|
64
|
+
const next = turnQueue.shift();
|
|
65
|
+
if (next) {
|
|
66
|
+
owner = next.slot;
|
|
67
|
+
active = next.slot;
|
|
68
|
+
next.start();
|
|
69
|
+
} else {
|
|
70
|
+
owner = void 0;
|
|
71
|
+
active = ROOT;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function runRequestExclusive(fn) {
|
|
75
|
+
if (nativeAsyncContext) return fn();
|
|
76
|
+
const slot = new RequestSlot();
|
|
77
|
+
await takeTurn(slot);
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
} finally {
|
|
81
|
+
await slot.idle();
|
|
82
|
+
passTurn(slot);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function awaitOutsideRequest(promise) {
|
|
86
|
+
if (nativeAsyncContext) return promise;
|
|
87
|
+
const slot = owner;
|
|
88
|
+
if (!slot || active !== slot) return promise;
|
|
89
|
+
passTurn(slot);
|
|
90
|
+
try {
|
|
91
|
+
return await promise;
|
|
92
|
+
} finally {
|
|
93
|
+
await takeTurn(slot);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
var AsyncLocalStorage = class {
|
|
97
|
+
native;
|
|
98
|
+
stacks = /* @__PURE__ */ new WeakMap();
|
|
99
|
+
constructor() {
|
|
100
|
+
if (nativeAsyncContext) this.native = new nativeAsyncContext.Variable();
|
|
101
|
+
}
|
|
102
|
+
run(store, callback, ...args) {
|
|
103
|
+
if (this.native) return this.native.run(store, callback, ...args);
|
|
104
|
+
const slot = active;
|
|
105
|
+
let stack = this.stacks.get(slot);
|
|
106
|
+
if (!stack) {
|
|
107
|
+
stack = [];
|
|
108
|
+
this.stacks.set(slot, stack);
|
|
109
|
+
}
|
|
110
|
+
const frame = { store };
|
|
111
|
+
stack.push(frame);
|
|
112
|
+
slot.enter();
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = callback(...args);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
this.leave(slot, frame);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (result instanceof Promise) {
|
|
121
|
+
return result.then(
|
|
122
|
+
(value) => {
|
|
123
|
+
if (!this.leave(slot, frame)) throw this.overlapError();
|
|
124
|
+
return value;
|
|
125
|
+
},
|
|
126
|
+
(error) => {
|
|
127
|
+
this.leave(slot, frame);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
this.leave(slot, frame);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
getStore() {
|
|
136
|
+
if (this.native) return this.native.get();
|
|
137
|
+
const slot = active;
|
|
138
|
+
if (slot.tainted.has(this)) throw this.overlapError();
|
|
139
|
+
const stack = this.stacks.get(slot);
|
|
140
|
+
return stack && stack.length > 0 ? stack[stack.length - 1].store : void 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Remove `frame` from its request's stack. Returns false when it was not the innermost frame: a
|
|
144
|
+
* later run of this storage in the same request was still open, so the two overlapped and reads
|
|
145
|
+
* in between may have seen the other run's store.
|
|
146
|
+
*/
|
|
147
|
+
leave(slot, frame) {
|
|
148
|
+
const stack = this.stacks.get(slot) ?? [];
|
|
149
|
+
const index = stack.lastIndexOf(frame);
|
|
150
|
+
const innermost = index === stack.length - 1;
|
|
151
|
+
if (index !== -1) stack.splice(index, 1);
|
|
152
|
+
if (!innermost) {
|
|
153
|
+
slot.tainted.add(this);
|
|
154
|
+
if (!overlapReported) {
|
|
155
|
+
overlapReported = true;
|
|
156
|
+
console.error(
|
|
157
|
+
"[frontmcp] Two concurrent runs of one async context overlapped inside a request. This runtime has no AsyncContext, so the context of each cannot be told apart; reads fail for the rest of that request instead of returning the wrong value."
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (stack.length === 0) slot.tainted.delete(this);
|
|
162
|
+
slot.leave();
|
|
163
|
+
return innermost;
|
|
164
|
+
}
|
|
165
|
+
overlapError() {
|
|
166
|
+
return new AsyncContextOverlapError(
|
|
167
|
+
"Concurrent runs of an async context overlapped in one request, and this runtime has no AsyncContext to tell them apart. Run the calls one after another, or use a runtime with AsyncContext."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
export {
|
|
172
|
+
AsyncContextOverlapError2 as AsyncContextOverlapError,
|
|
173
|
+
AsyncLocalStorage,
|
|
174
|
+
awaitOutsideRequest,
|
|
175
|
+
getAsyncContextMode,
|
|
176
|
+
runRequestExclusive
|
|
177
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// libs/utils/src/async-context/index.ts
|
|
2
|
+
import {
|
|
3
|
+
AsyncContextOverlapError,
|
|
4
|
+
AsyncLocalStorage,
|
|
5
|
+
awaitOutsideRequest,
|
|
6
|
+
getAsyncContextMode,
|
|
7
|
+
runRequestExclusive
|
|
8
|
+
} from "./browser-async-context.mjs";
|
|
9
|
+
export {
|
|
10
|
+
AsyncContextOverlapError,
|
|
11
|
+
AsyncLocalStorage,
|
|
12
|
+
awaitOutsideRequest,
|
|
13
|
+
getAsyncContextMode,
|
|
14
|
+
runRequestExclusive
|
|
15
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// libs/utils/src/class-brand/class-brand.ts
|
|
2
|
+
var CLASS_BRAND = /* @__PURE__ */ Symbol.for("@frontmcp/class-brand");
|
|
3
|
+
var ordinaryHasInstance = Function.prototype[Symbol.hasInstance];
|
|
4
|
+
function ownBrandOf(klass) {
|
|
5
|
+
if (typeof klass !== "function" || !Object.prototype.hasOwnProperty.call(klass, CLASS_BRAND)) return void 0;
|
|
6
|
+
return klass[CLASS_BRAND];
|
|
7
|
+
}
|
|
8
|
+
function classOfPrototype(proto) {
|
|
9
|
+
return Object.getOwnPropertyDescriptor(proto, "constructor")?.value;
|
|
10
|
+
}
|
|
11
|
+
function brandClass(klass, brand) {
|
|
12
|
+
Object.defineProperty(klass, CLASS_BRAND, { value: brand, enumerable: false, writable: false, configurable: false });
|
|
13
|
+
}
|
|
14
|
+
function isBrandedInstance(klass, value) {
|
|
15
|
+
if (ordinaryHasInstance.call(klass, value)) return true;
|
|
16
|
+
if (typeof value !== "object" || value === null) return false;
|
|
17
|
+
const brand = ownBrandOf(klass);
|
|
18
|
+
if (brand === void 0) return false;
|
|
19
|
+
for (let proto = Object.getPrototypeOf(value); proto !== null; proto = Object.getPrototypeOf(proto)) {
|
|
20
|
+
if (ownBrandOf(classOfPrototype(proto)) === brand) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
brandClass,
|
|
26
|
+
isBrandedInstance
|
|
27
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// libs/utils/src/content/content.ts
|
|
2
|
+
function sanitizeToJson(value) {
|
|
3
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
4
|
+
function withinAncestors(container, build) {
|
|
5
|
+
if (ancestors.has(container)) {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
ancestors.add(container);
|
|
9
|
+
try {
|
|
10
|
+
return build();
|
|
11
|
+
} finally {
|
|
12
|
+
ancestors.delete(container);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function sanitize(val) {
|
|
16
|
+
if (typeof val === "function" || typeof val === "symbol") {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
if (typeof val === "bigint") {
|
|
20
|
+
return val.toString();
|
|
21
|
+
}
|
|
22
|
+
if (val instanceof Date) {
|
|
23
|
+
return val.toISOString();
|
|
24
|
+
}
|
|
25
|
+
if (val instanceof Error) {
|
|
26
|
+
return {
|
|
27
|
+
name: val.name,
|
|
28
|
+
message: val.message,
|
|
29
|
+
stack: val.stack
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (val instanceof Map) {
|
|
33
|
+
return withinAncestors(val, () => {
|
|
34
|
+
const obj = {};
|
|
35
|
+
for (const [k, v] of val.entries()) {
|
|
36
|
+
obj[String(k)] = sanitize(v);
|
|
37
|
+
}
|
|
38
|
+
return obj;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (val instanceof Set) {
|
|
42
|
+
return withinAncestors(val, () => Array.from(val).map(sanitize));
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(val)) {
|
|
45
|
+
return withinAncestors(val, () => val.map(sanitize));
|
|
46
|
+
}
|
|
47
|
+
if (val && typeof val === "object") {
|
|
48
|
+
return withinAncestors(val, () => {
|
|
49
|
+
const sanitized = {};
|
|
50
|
+
for (const [key, value2] of Object.entries(val)) {
|
|
51
|
+
const clean = sanitize(value2);
|
|
52
|
+
if (clean !== void 0) {
|
|
53
|
+
sanitized[key] = clean;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return sanitized;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return val;
|
|
60
|
+
}
|
|
61
|
+
return sanitize(value);
|
|
62
|
+
}
|
|
63
|
+
function findNonFiniteNumber(value) {
|
|
64
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
65
|
+
function walk(val, path) {
|
|
66
|
+
if (typeof val === "number" && !Number.isFinite(val)) {
|
|
67
|
+
return { path, value: val };
|
|
68
|
+
}
|
|
69
|
+
if (val && typeof val === "object") {
|
|
70
|
+
if (seen.has(val)) return void 0;
|
|
71
|
+
seen.add(val);
|
|
72
|
+
if (Array.isArray(val)) {
|
|
73
|
+
for (let i = 0; i < val.length; i++) {
|
|
74
|
+
const found = walk(val[i], `${path}[${i}]`);
|
|
75
|
+
if (found) return found;
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
for (const [k, v] of Object.entries(val)) {
|
|
80
|
+
const found = walk(v, path ? `${path}.${k}` : k);
|
|
81
|
+
if (found) return found;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
return walk(value, "");
|
|
87
|
+
}
|
|
88
|
+
function findDroppedKeys(original, parsed) {
|
|
89
|
+
const droppedPaths = /* @__PURE__ */ new Set();
|
|
90
|
+
function walk(source, target, path) {
|
|
91
|
+
if (source === target) return;
|
|
92
|
+
if (Array.isArray(source)) {
|
|
93
|
+
source.forEach((item, index) => walk(item, Array.isArray(target) ? target[index] : void 0, `${path}[]`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!isPlainRecord(source)) return;
|
|
97
|
+
const targetRecord = isPlainRecord(target) ? target : {};
|
|
98
|
+
for (const [key, value] of Object.entries(source)) {
|
|
99
|
+
if (value === void 0) continue;
|
|
100
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
101
|
+
if (Object.prototype.hasOwnProperty.call(targetRecord, key)) walk(value, targetRecord[key], keyPath);
|
|
102
|
+
else droppedPaths.add(keyPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
walk(original, parsed, "");
|
|
106
|
+
return [...droppedPaths].sort();
|
|
107
|
+
}
|
|
108
|
+
function isPlainRecord(value) {
|
|
109
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
var MIME_TYPES = {
|
|
112
|
+
json: "application/json",
|
|
113
|
+
xml: "application/xml",
|
|
114
|
+
html: "text/html",
|
|
115
|
+
htm: "text/html",
|
|
116
|
+
css: "text/css",
|
|
117
|
+
js: "application/javascript",
|
|
118
|
+
ts: "application/typescript",
|
|
119
|
+
txt: "text/plain",
|
|
120
|
+
md: "text/markdown",
|
|
121
|
+
yaml: "application/yaml",
|
|
122
|
+
yml: "application/yaml",
|
|
123
|
+
csv: "text/csv",
|
|
124
|
+
png: "image/png",
|
|
125
|
+
jpg: "image/jpeg",
|
|
126
|
+
jpeg: "image/jpeg",
|
|
127
|
+
gif: "image/gif",
|
|
128
|
+
svg: "image/svg+xml",
|
|
129
|
+
pdf: "application/pdf"
|
|
130
|
+
};
|
|
131
|
+
function inferMimeType(uri, content) {
|
|
132
|
+
const ext = uri.split(".").pop()?.toLowerCase();
|
|
133
|
+
if (ext && MIME_TYPES[ext]) {
|
|
134
|
+
return MIME_TYPES[ext];
|
|
135
|
+
}
|
|
136
|
+
if (typeof content === "string") {
|
|
137
|
+
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
|
|
138
|
+
return "application/json";
|
|
139
|
+
}
|
|
140
|
+
if (content.trim().startsWith("<")) {
|
|
141
|
+
return content.includes("<!DOCTYPE html") || content.includes("<html") ? "text/html" : "application/xml";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return "text/plain";
|
|
145
|
+
}
|
|
146
|
+
export {
|
|
147
|
+
findDroppedKeys,
|
|
148
|
+
findNonFiniteNumber,
|
|
149
|
+
inferMimeType,
|
|
150
|
+
sanitizeToJson
|
|
151
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// libs/utils/src/crypto/browser.ts
|
|
2
|
+
import { gcm } from "@noble/ciphers/aes.js";
|
|
3
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
4
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
5
|
+
import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
|
|
6
|
+
import { randomBytes as nobleRandomBytes } from "@noble/hashes/utils.js";
|
|
7
|
+
import { isRsaPssAlg, jwtAlgToWebCryptoAlg } from "./jwt-alg.mjs";
|
|
8
|
+
import { rsaVerifyBrowser } from "./web-rsa.mjs";
|
|
9
|
+
function toBytes(data) {
|
|
10
|
+
if (typeof data === "string") {
|
|
11
|
+
return new TextEncoder().encode(data);
|
|
12
|
+
}
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
function toHex(bytes) {
|
|
16
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
17
|
+
}
|
|
18
|
+
function generateUUID() {
|
|
19
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
20
|
+
return crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
const bytes = nobleRandomBytes(16);
|
|
23
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
24
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
25
|
+
const hex = toHex(bytes);
|
|
26
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
27
|
+
}
|
|
28
|
+
function constantTimeEqual(a, b) {
|
|
29
|
+
if (a.length !== b.length) return false;
|
|
30
|
+
let result = 0;
|
|
31
|
+
for (let i = 0; i < a.length; i++) {
|
|
32
|
+
result |= a[i] ^ b[i];
|
|
33
|
+
}
|
|
34
|
+
return result === 0;
|
|
35
|
+
}
|
|
36
|
+
var browserCrypto = {
|
|
37
|
+
randomUUID() {
|
|
38
|
+
return generateUUID();
|
|
39
|
+
},
|
|
40
|
+
randomBytes(length) {
|
|
41
|
+
return nobleRandomBytes(length);
|
|
42
|
+
},
|
|
43
|
+
sha256(data) {
|
|
44
|
+
return sha256Hash(toBytes(data));
|
|
45
|
+
},
|
|
46
|
+
sha256Hex(data) {
|
|
47
|
+
return toHex(sha256Hash(toBytes(data)));
|
|
48
|
+
},
|
|
49
|
+
hmacSha256(key, data) {
|
|
50
|
+
return hmac(sha256Hash, key, data);
|
|
51
|
+
},
|
|
52
|
+
hkdfSha256(ikm, salt, info, length) {
|
|
53
|
+
const effectiveSalt = salt.length > 0 ? salt : new Uint8Array(32);
|
|
54
|
+
return hkdf(sha256Hash, ikm, effectiveSalt, info, length);
|
|
55
|
+
},
|
|
56
|
+
encryptAesGcm(key, plaintext, iv) {
|
|
57
|
+
const cipher = gcm(key, iv);
|
|
58
|
+
const sealed = cipher.encrypt(plaintext);
|
|
59
|
+
const ciphertext = sealed.slice(0, -16);
|
|
60
|
+
const tag = sealed.slice(-16);
|
|
61
|
+
return { ciphertext, tag };
|
|
62
|
+
},
|
|
63
|
+
decryptAesGcm(key, ciphertext, iv, tag) {
|
|
64
|
+
const cipher = gcm(key, iv);
|
|
65
|
+
const sealed = new Uint8Array(ciphertext.length + tag.length);
|
|
66
|
+
sealed.set(ciphertext);
|
|
67
|
+
sealed.set(tag, ciphertext.length);
|
|
68
|
+
return cipher.decrypt(sealed);
|
|
69
|
+
},
|
|
70
|
+
timingSafeEqual(a, b) {
|
|
71
|
+
return constantTimeEqual(a, b);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function nodeOnly(name) {
|
|
75
|
+
throw new Error(`${name} is only available in Node.js runtimes`);
|
|
76
|
+
}
|
|
77
|
+
function rsaVerify() {
|
|
78
|
+
return nodeOnly("rsaVerify");
|
|
79
|
+
}
|
|
80
|
+
function rsaSignBase64Url() {
|
|
81
|
+
return nodeOnly("rsaSignBase64Url");
|
|
82
|
+
}
|
|
83
|
+
function rsaVerifySync() {
|
|
84
|
+
return nodeOnly("rsaVerifySync");
|
|
85
|
+
}
|
|
86
|
+
function pemToPublicJwk() {
|
|
87
|
+
return nodeOnly("pemToPublicJwk");
|
|
88
|
+
}
|
|
89
|
+
function generateRsaKeyPair() {
|
|
90
|
+
return nodeOnly("generateRsaKeyPair");
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
browserCrypto,
|
|
94
|
+
browserCrypto as cryptoProvider,
|
|
95
|
+
generateRsaKeyPair,
|
|
96
|
+
isRsaPssAlg,
|
|
97
|
+
jwtAlgToWebCryptoAlg,
|
|
98
|
+
pemToPublicJwk,
|
|
99
|
+
rsaSignBase64Url,
|
|
100
|
+
rsaVerify,
|
|
101
|
+
rsaVerifyBrowser,
|
|
102
|
+
rsaVerifySync
|
|
103
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// libs/utils/src/crypto/encrypted-blob.ts
|
|
2
|
+
import { encryptAesGcm, decryptAesGcm, randomBytes, base64urlEncode, base64urlDecode } from "./index.mjs";
|
|
3
|
+
var textEncoder = new TextEncoder();
|
|
4
|
+
var textDecoder = new TextDecoder();
|
|
5
|
+
var EncryptedBlobError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "EncryptedBlobError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function encryptValue(data, key) {
|
|
12
|
+
if (key.length !== 32) {
|
|
13
|
+
throw new EncryptedBlobError(`Encryption key must be 32 bytes (AES-256), got ${key.length} bytes`);
|
|
14
|
+
}
|
|
15
|
+
let jsonString;
|
|
16
|
+
try {
|
|
17
|
+
jsonString = JSON.stringify(data);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new EncryptedBlobError(`Failed to serialize data: ${error.message}`);
|
|
20
|
+
}
|
|
21
|
+
const plaintext = textEncoder.encode(jsonString);
|
|
22
|
+
const iv = randomBytes(12);
|
|
23
|
+
const { ciphertext, tag } = encryptAesGcm(key, plaintext, iv);
|
|
24
|
+
return {
|
|
25
|
+
alg: "A256GCM",
|
|
26
|
+
iv: base64urlEncode(iv),
|
|
27
|
+
tag: base64urlEncode(tag),
|
|
28
|
+
data: base64urlEncode(ciphertext)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function decryptValue(blob, key) {
|
|
32
|
+
if (key.length !== 32) {
|
|
33
|
+
throw new EncryptedBlobError(`Decryption key must be 32 bytes (AES-256), got ${key.length} bytes`);
|
|
34
|
+
}
|
|
35
|
+
if (blob.alg !== "A256GCM") {
|
|
36
|
+
throw new EncryptedBlobError(`Unsupported algorithm: ${blob.alg}, expected A256GCM`);
|
|
37
|
+
}
|
|
38
|
+
let iv;
|
|
39
|
+
let tag;
|
|
40
|
+
let ciphertext;
|
|
41
|
+
try {
|
|
42
|
+
iv = base64urlDecode(blob.iv);
|
|
43
|
+
tag = base64urlDecode(blob.tag);
|
|
44
|
+
ciphertext = base64urlDecode(blob.data);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new EncryptedBlobError(`Failed to decode blob fields: ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
let decrypted;
|
|
49
|
+
try {
|
|
50
|
+
decrypted = decryptAesGcm(key, ciphertext, iv, tag);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new EncryptedBlobError(`Decryption failed: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(textDecoder.decode(decrypted));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new EncryptedBlobError(`Failed to parse decrypted data: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function tryDecryptValue(blob, key) {
|
|
61
|
+
try {
|
|
62
|
+
return decryptValue(blob, key);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function serializeBlob(blob) {
|
|
68
|
+
return JSON.stringify(blob);
|
|
69
|
+
}
|
|
70
|
+
function deserializeBlob(str) {
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(str);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new EncryptedBlobError(`Failed to parse blob JSON: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
if (!isValidEncryptedBlob(parsed)) {
|
|
78
|
+
throw new EncryptedBlobError("Invalid encrypted blob structure");
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
function tryDeserializeBlob(str) {
|
|
83
|
+
try {
|
|
84
|
+
return deserializeBlob(str);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isValidEncryptedBlob(value) {
|
|
90
|
+
return typeof value === "object" && value !== null && "alg" in value && "iv" in value && "tag" in value && "data" in value && value.alg === "A256GCM" && typeof value.iv === "string" && typeof value.tag === "string" && typeof value.data === "string";
|
|
91
|
+
}
|
|
92
|
+
function encryptAndSerialize(data, key) {
|
|
93
|
+
const blob = encryptValue(data, key);
|
|
94
|
+
return serializeBlob(blob);
|
|
95
|
+
}
|
|
96
|
+
function deserializeAndDecrypt(str, key) {
|
|
97
|
+
const blob = deserializeBlob(str);
|
|
98
|
+
return decryptValue(blob, key);
|
|
99
|
+
}
|
|
100
|
+
function tryDeserializeAndDecrypt(str, key) {
|
|
101
|
+
try {
|
|
102
|
+
return deserializeAndDecrypt(str, key);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
EncryptedBlobError,
|
|
109
|
+
decryptValue,
|
|
110
|
+
deserializeAndDecrypt,
|
|
111
|
+
deserializeBlob,
|
|
112
|
+
encryptAndSerialize,
|
|
113
|
+
encryptValue,
|
|
114
|
+
isValidEncryptedBlob,
|
|
115
|
+
serializeBlob,
|
|
116
|
+
tryDecryptValue,
|
|
117
|
+
tryDeserializeAndDecrypt,
|
|
118
|
+
tryDeserializeBlob
|
|
119
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// libs/utils/src/crypto/hmac-signing.ts
|
|
2
|
+
import { hmacSha256 } from "./index.mjs";
|
|
3
|
+
import { base64urlEncode, base64urlDecode } from "./index.mjs";
|
|
4
|
+
import { timingSafeEqual } from "./index.mjs";
|
|
5
|
+
function computeSignature(data, secret) {
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const keyBytes = encoder.encode(secret);
|
|
8
|
+
const dataBytes = encoder.encode(data);
|
|
9
|
+
const hmac = hmacSha256(keyBytes, dataBytes);
|
|
10
|
+
return base64urlEncode(hmac);
|
|
11
|
+
}
|
|
12
|
+
function signData(data, config) {
|
|
13
|
+
const jsonData = JSON.stringify(data);
|
|
14
|
+
const sig = computeSignature(jsonData, config.secret);
|
|
15
|
+
const signed = {
|
|
16
|
+
data,
|
|
17
|
+
sig,
|
|
18
|
+
v: 1
|
|
19
|
+
};
|
|
20
|
+
return JSON.stringify(signed);
|
|
21
|
+
}
|
|
22
|
+
function verifyData(signedJson, config) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(signedJson);
|
|
25
|
+
if (!parsed || typeof parsed !== "object" || !("sig" in parsed)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const signed = parsed;
|
|
29
|
+
if (signed.v !== 1) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const jsonData = JSON.stringify(signed.data);
|
|
33
|
+
const expectedSig = computeSignature(jsonData, config.secret);
|
|
34
|
+
const sigBytes = base64urlDecode(signed.sig);
|
|
35
|
+
const expectedBytes = base64urlDecode(expectedSig);
|
|
36
|
+
if (sigBytes.length !== expectedBytes.length) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!timingSafeEqual(sigBytes, expectedBytes)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return signed.data;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isSignedData(json) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(json);
|
|
50
|
+
return parsed && typeof parsed === "object" && "sig" in parsed && "v" in parsed;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function verifyOrParseData(json, config) {
|
|
56
|
+
if (isSignedData(json)) {
|
|
57
|
+
return verifyData(json, config);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(json);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
isSignedData,
|
|
67
|
+
signData,
|
|
68
|
+
verifyData,
|
|
69
|
+
verifyOrParseData
|
|
70
|
+
};
|