@mobileaidev/ai-app-bridge 0.2.15 → 0.3.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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- package/skills/ai-app-bridge-use/SKILL.md +23 -360
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
5
|
+
const {
|
|
6
|
+
isAndroidUiElementOpeningTag,
|
|
7
|
+
looksLikeAndroidUiHierarchyXml,
|
|
8
|
+
replaceAndroidUiOpeningTags,
|
|
9
|
+
} = require('./android-uia-xml');
|
|
10
|
+
|
|
11
|
+
const REDACTED = '[REDACTED]';
|
|
12
|
+
const SECURE_UI_XML_TEXT_ATTRIBUTES = new Set([
|
|
13
|
+
'text',
|
|
14
|
+
'value',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Canonical write-side normalization shared by every FactStore Adapter.
|
|
19
|
+
* Only password and token values are redacted here. Bodies, cookies, and
|
|
20
|
+
* ordinary query parameters stay as captured.
|
|
21
|
+
*/
|
|
22
|
+
function normalizePersistentFact(fact, now = Date.now()) {
|
|
23
|
+
if (!fact || typeof fact !== 'object' || Array.isArray(fact)) {
|
|
24
|
+
throw new TypeError('fact must be an object');
|
|
25
|
+
}
|
|
26
|
+
const partition = requiredString(fact.partition, 'partition');
|
|
27
|
+
const timestamps = fact.timestamps && typeof fact.timestamps === 'object' ? fact.timestamps : {};
|
|
28
|
+
const payload = fact.payload === undefined ? null : fact.payload;
|
|
29
|
+
return {
|
|
30
|
+
partition,
|
|
31
|
+
targetKey: requiredString(fact.targetKey, 'targetKey'),
|
|
32
|
+
app: sanitizePersistentValue(fact.app === undefined ? {} : fact.app, 'app'),
|
|
33
|
+
runtimeEpoch: requiredString(fact.runtimeEpoch, 'runtimeEpoch'),
|
|
34
|
+
actionId: fact.actionId === undefined || fact.actionId === null ? null : String(fact.actionId),
|
|
35
|
+
dedupeKey: fact.dedupeKey === undefined || fact.dedupeKey === null
|
|
36
|
+
? null
|
|
37
|
+
: normalizeDedupeKey(fact.dedupeKey),
|
|
38
|
+
timestamps: {
|
|
39
|
+
occurredAtMs: finiteInteger(timestamps.occurredAtMs, now, 'timestamps.occurredAtMs'),
|
|
40
|
+
observedAtMs: finiteInteger(timestamps.observedAtMs, now, 'timestamps.observedAtMs'),
|
|
41
|
+
ingestedAtMs: now,
|
|
42
|
+
},
|
|
43
|
+
payload: sanitizePersistentValue(payload, 'payload'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sanitizePersistentValue(value, key = '', ancestors = new WeakSet()) {
|
|
48
|
+
if (isSensitiveKey(key)) return REDACTED;
|
|
49
|
+
if (value === null || value === undefined) return value === undefined ? null : value;
|
|
50
|
+
if (typeof value === 'string') return sanitizeString(value, key);
|
|
51
|
+
if (typeof value === 'bigint') return value.toString();
|
|
52
|
+
if (typeof value !== 'object') return value;
|
|
53
|
+
if (value instanceof Date) return value.toISOString();
|
|
54
|
+
if (Buffer.isBuffer(value)) return { type: 'buffer', byteLength: value.length };
|
|
55
|
+
if (ancestors.has(value)) throw new TypeError('fact must be JSON-serializable without cycles');
|
|
56
|
+
ancestors.add(value);
|
|
57
|
+
let result;
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
result = value.map((entry) => sanitizePersistentValue(entry, key, ancestors));
|
|
60
|
+
} else {
|
|
61
|
+
result = {};
|
|
62
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
63
|
+
result[childKey] = sanitizePersistentValue(childValue, childKey, ancestors);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
ancestors.delete(value);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sanitizeString(value, key) {
|
|
71
|
+
if (isCanonicalBase64Field(key, value)) return value;
|
|
72
|
+
const androidUiHierarchyXml = looksLikeAndroidUiHierarchyXml(value);
|
|
73
|
+
if (androidUiHierarchyXml) value = redactSecureAndroidUiXml(value);
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(trimmed);
|
|
78
|
+
if (parsed && typeof parsed === 'object') {
|
|
79
|
+
const sanitized = sanitizePersistentValue(parsed);
|
|
80
|
+
// JSON can itself be captured text, including a hashed device receipt.
|
|
81
|
+
// Preserve its bytes when no redaction is needed. Duplicate keys must
|
|
82
|
+
// still be collapsed: JSON.parse may have hidden an earlier secret.
|
|
83
|
+
if (!isDeepStrictEqual(parsed, sanitized) || hasDuplicateJsonKeys(trimmed)) value = JSON.stringify(sanitized);
|
|
84
|
+
}
|
|
85
|
+
} catch (_) {
|
|
86
|
+
// Preserve non-JSON text and apply inline credential rules below.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (looksLikeUrlKey(key)) {
|
|
90
|
+
// Some frameworks place credentials in the path instead of a query
|
|
91
|
+
// parameter (for example Moodle tokenpluginfile.php/<token>/...). Apply
|
|
92
|
+
// this once before parsing as well so relative URLs receive the same
|
|
93
|
+
// write-side protection as absolute URLs.
|
|
94
|
+
value = redactSensitiveUrlPath(value);
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(value);
|
|
97
|
+
if (url.password) url.password = REDACTED;
|
|
98
|
+
url.pathname = redactSensitiveUrlPath(url.pathname);
|
|
99
|
+
for (const parameter of [...url.searchParams.keys()]) {
|
|
100
|
+
if (isSensitiveKey(parameter)) url.searchParams.set(parameter, REDACTED);
|
|
101
|
+
}
|
|
102
|
+
value = url.toString();
|
|
103
|
+
} catch (_) {
|
|
104
|
+
// Relative/non-URL text still receives assignment redaction below.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let sanitized = value.replace(/\b(Bearer|Basic)\s+[^\s,;]+/gi, '$1 [REDACTED]');
|
|
108
|
+
sanitized = sanitized.replace(
|
|
109
|
+
/(\b(?:authorization|proxy[-_ ]?authorization)\b\s*[:=]\s*)[^\r\n]*/gi,
|
|
110
|
+
'$1[REDACTED]',
|
|
111
|
+
);
|
|
112
|
+
const assignmentPattern = /((?:["']?)(?:password|passwd|passcode|pwd|access[-_ ]?token|refresh[-_ ]?token|id[-_ ]?token|session[-_ ]?token|token)(?:["']?)\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;&#"'{}\[\]]+)/gi;
|
|
113
|
+
sanitized = sanitized.replace(assignmentPattern, (match, prefix, secretValue, offset, source) => {
|
|
114
|
+
const unquotedValue = secretValue.replace(/^["']|["']$/g, '');
|
|
115
|
+
if (
|
|
116
|
+
androidUiHierarchyXml
|
|
117
|
+
&& isUiXmlBooleanPasswordAttribute(source, offset, prefix, unquotedValue)
|
|
118
|
+
) return match;
|
|
119
|
+
if (unquotedValue === REDACTED || /^%5bredacted%5d$/i.test(unquotedValue)) return match;
|
|
120
|
+
const quote = secretValue.length >= 2
|
|
121
|
+
&& (secretValue[0] === '"' || secretValue[0] === "'")
|
|
122
|
+
&& secretValue.at(-1) === secretValue[0]
|
|
123
|
+
? secretValue[0]
|
|
124
|
+
: '';
|
|
125
|
+
return `${prefix}${quote}${REDACTED}${quote}`;
|
|
126
|
+
});
|
|
127
|
+
return sanitized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasDuplicateJsonKeys(json) {
|
|
131
|
+
// JSON.parse has already validated syntax. Keep quoted strings whole so a
|
|
132
|
+
// brace or colon inside a value cannot masquerade as an object/key token.
|
|
133
|
+
const tokens = json.match(/"(?:[^"\\]|\\.)*"|[{}\[\],:]/g) || [];
|
|
134
|
+
const objects = [];
|
|
135
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
136
|
+
if (tokens[i] === '{') objects.push(new Set());
|
|
137
|
+
else if (tokens[i] === '}') objects.pop();
|
|
138
|
+
else if (tokens[i + 1] === ':') {
|
|
139
|
+
const key = JSON.parse(tokens[i]);
|
|
140
|
+
if (objects.at(-1).has(key)) return true;
|
|
141
|
+
objects.at(-1).add(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function redactSecureAndroidUiXml(value) {
|
|
148
|
+
return replaceAndroidUiOpeningTags(value, (tag) => {
|
|
149
|
+
const attributes = uiXmlAttributes(tag);
|
|
150
|
+
const inputShape = [
|
|
151
|
+
attributes.class,
|
|
152
|
+
attributes.classname,
|
|
153
|
+
attributes.type,
|
|
154
|
+
attributes.role,
|
|
155
|
+
].some((candidate) => (
|
|
156
|
+
/edittext|textfield|textinput|securefield|searchfield|^input$|^textarea$|textbox/i
|
|
157
|
+
.test(String(candidate || ''))
|
|
158
|
+
));
|
|
159
|
+
const sensitiveIdentity = inputShape && [
|
|
160
|
+
attributes.resourceid,
|
|
161
|
+
attributes.resourcename,
|
|
162
|
+
attributes.id,
|
|
163
|
+
attributes.name,
|
|
164
|
+
attributes.autocomplete,
|
|
165
|
+
attributes.textcontenttype,
|
|
166
|
+
attributes.placeholder,
|
|
167
|
+
].some((candidate) => (
|
|
168
|
+
/password|passwd|pwd|passcode|current-password|new-password/i
|
|
169
|
+
.test(String(candidate || ''))
|
|
170
|
+
));
|
|
171
|
+
const secure = isTruePrivacyFlag(attributes.password)
|
|
172
|
+
|| isTruePrivacyFlag(attributes.ispassword)
|
|
173
|
+
|| isTruePrivacyFlag(attributes.secure)
|
|
174
|
+
|| isTruePrivacyFlag(attributes.issecure)
|
|
175
|
+
|| /securetext|passwordfield|passwordinput/i.test(String(attributes.class || ''))
|
|
176
|
+
|| sensitiveIdentity;
|
|
177
|
+
if (!secure) return tag;
|
|
178
|
+
return tag.replace(
|
|
179
|
+
/([A-Za-z_:][A-Za-z0-9_.:-]*)(\s*=\s*)(["'])([\s\S]*?)\3/g,
|
|
180
|
+
(attribute, name, separator, quote) => (
|
|
181
|
+
SECURE_UI_XML_TEXT_ATTRIBUTES.has(normalizeXmlAttributeName(name))
|
|
182
|
+
? `${name}${separator}${quote}${REDACTED}${quote}`
|
|
183
|
+
: attribute
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function uiXmlAttributes(tag) {
|
|
190
|
+
const attributes = {};
|
|
191
|
+
const pattern = /([A-Za-z_:][A-Za-z0-9_.:-]*)(?:\s*=\s*)(["'])([\s\S]*?)\2/g;
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = pattern.exec(tag)) !== null) {
|
|
194
|
+
attributes[normalizeXmlAttributeName(match[1])] = match[3];
|
|
195
|
+
}
|
|
196
|
+
return attributes;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeXmlAttributeName(value) {
|
|
200
|
+
return String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isTruePrivacyFlag(value) {
|
|
204
|
+
return value === true || value === 1 || String(value).toLowerCase() === 'true';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isUiXmlBooleanPasswordAttribute(source, offset, prefix, value) {
|
|
208
|
+
if (!/^(?:true|false)$/i.test(String(value))) return false;
|
|
209
|
+
if (!/^["']?password["']?\s*[:=]\s*$/i.test(String(prefix).trim())) return false;
|
|
210
|
+
const preceding = offset > 0 ? source[offset - 1] : '';
|
|
211
|
+
if (preceding && !/[\s<]/.test(preceding)) return false;
|
|
212
|
+
const before = source.slice(0, offset);
|
|
213
|
+
const open = before.lastIndexOf('<');
|
|
214
|
+
const close = before.lastIndexOf('>');
|
|
215
|
+
return open > close && isAndroidUiElementOpeningTag(source.slice(open));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isCanonicalBase64Field(key, value) {
|
|
219
|
+
const normalizedKey = String(key || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
220
|
+
return normalizedKey.endsWith('base64')
|
|
221
|
+
&& value.length % 4 === 0
|
|
222
|
+
&& /^[A-Za-z0-9+/]*={0,2}$/.test(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function redactSensitiveUrlPath(value) {
|
|
226
|
+
let redacted = String(value);
|
|
227
|
+
|
|
228
|
+
// Moodle's authenticated file endpoint embeds the access token directly
|
|
229
|
+
// after the script name, without a descriptive "token" path segment.
|
|
230
|
+
redacted = redacted.replace(
|
|
231
|
+
/(\/tokenpluginfile\.php\/)[^/?#]+/gi,
|
|
232
|
+
`$1${REDACTED}`,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Cover conventional token marker/value path pairs and marker=value segments.
|
|
236
|
+
const marker = '(?:access[-_]?token|refresh[-_]?token|id[-_]?token|session[-_]?token|token)';
|
|
237
|
+
redacted = redacted.replace(
|
|
238
|
+
new RegExp(`(\\/(?:oauth|authorization|auth)\\/token\\/)[^/?#]+`, 'gi'),
|
|
239
|
+
`$1${REDACTED}`,
|
|
240
|
+
);
|
|
241
|
+
redacted = redacted.replace(
|
|
242
|
+
new RegExp(`(\\/${marker}\\/)[^/?#]+`, 'gi'),
|
|
243
|
+
(match, prefix) => {
|
|
244
|
+
const valueSegment = match.slice(prefix.length);
|
|
245
|
+
// Do not consume the second marker in paths such as /auth/code/value;
|
|
246
|
+
// the nested-marker rule above has already handled the actual value.
|
|
247
|
+
return new RegExp(`^${marker}$`, 'i').test(valueSegment)
|
|
248
|
+
? match
|
|
249
|
+
: `${prefix}${REDACTED}`;
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
redacted = redacted.replace(
|
|
253
|
+
new RegExp(`(\\/${marker}[=:])[^/?#]+`, 'gi'),
|
|
254
|
+
`$1${REDACTED}`,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// JWTs are self-identifying credentials and occasionally appear as an
|
|
258
|
+
// otherwise unlabeled URL segment.
|
|
259
|
+
redacted = redacted.replace(
|
|
260
|
+
/(\/)(eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,})(?=\/|\?|#|$)/g,
|
|
261
|
+
`$1${REDACTED}`,
|
|
262
|
+
);
|
|
263
|
+
return redacted;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isPasswordKey(key) {
|
|
267
|
+
const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
268
|
+
return normalized === 'password'
|
|
269
|
+
|| normalized === 'passwd'
|
|
270
|
+
|| normalized === 'pwd'
|
|
271
|
+
|| normalized === 'passcode'
|
|
272
|
+
|| normalized.endsWith('password');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isTokenKey(key) {
|
|
276
|
+
const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
277
|
+
return normalized === 'token'
|
|
278
|
+
|| normalized === 'accesstoken'
|
|
279
|
+
|| normalized === 'refreshtoken'
|
|
280
|
+
|| normalized === 'idtoken'
|
|
281
|
+
|| normalized === 'sessiontoken'
|
|
282
|
+
|| normalized === 'authorization'
|
|
283
|
+
|| normalized === 'proxyauthorization'
|
|
284
|
+
|| normalized.endsWith('token');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isSensitiveKey(key) {
|
|
288
|
+
return isPasswordKey(key) || isTokenKey(key);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function looksLikeUrlKey(key) {
|
|
292
|
+
const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
293
|
+
return normalized === 'url' || normalized === 'uri' || normalized.endsWith('url') || normalized.endsWith('uri');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeDedupeKey(value) {
|
|
297
|
+
return crypto.createHash('sha256')
|
|
298
|
+
.update(`fact-dedupe-v1\0${String(value)}`)
|
|
299
|
+
.digest('base64url');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function requiredString(value, name) {
|
|
303
|
+
if (value === undefined || value === null || String(value).length === 0) {
|
|
304
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
305
|
+
}
|
|
306
|
+
return String(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function finiteInteger(value, fallback, name) {
|
|
310
|
+
const candidate = value === undefined ? fallback : Number(value);
|
|
311
|
+
if (!Number.isSafeInteger(candidate)) throw new TypeError(`${name} must be a safe integer`);
|
|
312
|
+
return candidate;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
REDACTED,
|
|
317
|
+
isSensitiveKey,
|
|
318
|
+
normalizeDedupeKey,
|
|
319
|
+
normalizePersistentFact,
|
|
320
|
+
sanitizePersistentValue,
|
|
321
|
+
};
|