@deepexi/datasense-connector 3.0.1 → 3.1.0
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 +158 -139
- package/package.json +19 -28
- package/scripts/build.ts +188 -0
- package/src/define.ts +267 -0
- package/src/descriptor.ts +116 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +108 -0
- package/src/internal/location.ts +23 -0
- package/src/runtime/guest/bridge.ts +238 -0
- package/src/runtime/guest/cache.ts +43 -0
- package/src/runtime/guest/check.ts +17 -0
- package/src/runtime/guest/crypto.ts +166 -0
- package/src/runtime/guest/ctx.ts +157 -0
- package/src/runtime/guest/db.ts +59 -0
- package/src/runtime/guest/failure.ts +69 -0
- package/src/runtime/guest/http.ts +194 -0
- package/src/runtime/guest/index.ts +102 -0
- package/{runtime/guest/limits.d.ts → src/runtime/guest/limits.ts} +12 -12
- package/src/runtime/guest/loader.ts +210 -0
- package/src/runtime/guest/log.ts +47 -0
- package/src/runtime/guest/map.ts +45 -0
- package/src/runtime/guest/operation.ts +193 -0
- package/src/runtime/guest/paginate.ts +169 -0
- package/src/runtime/guest/pull.ts +177 -0
- package/src/runtime/guest/push.ts +92 -0
- package/src/runtime/guest/signal.ts +14 -0
- package/src/runtime/guest/suggest.ts +51 -0
- package/src/runtime/guest/time.ts +137 -0
- package/src/runtime/guest-runtime.ts +75 -0
- package/src/runtime/index.ts +53 -0
- package/src/runtime/protocol.ts +557 -0
- package/src/runtime/session.ts +216 -0
- package/src/testing/child.ts +21 -0
- package/src/testing/compliance.ts +343 -0
- package/src/testing/engine.ts +445 -0
- package/src/testing/fixtures.ts +591 -0
- package/src/testing/gateway.ts +482 -0
- package/src/testing/import-guard.ts +221 -0
- package/src/testing/index.ts +43 -0
- package/src/testing/local-db.ts +226 -0
- package/src/testing/mock-external-system.ts +249 -0
- package/src/testing/pinned-http.ts +144 -0
- package/src/testing/recorder.ts +140 -0
- package/src/testing/redaction.ts +94 -0
- package/src/testing/replay.ts +437 -0
- package/src/testing/run-tests.ts +115 -0
- package/src/testing/schema.ts +70 -0
- package/src/testing/source-loader.ts +374 -0
- package/src/testing/types.ts +78 -0
- package/src/types.ts +329 -0
- package/src/validate.ts +267 -0
- package/test/build.test.ts +159 -0
- package/test/define.test-d.ts +182 -0
- package/test/fixtures/acceptance.ts +156 -0
- package/test/fixtures/connectors/http-pull/.ontos/descriptor.json +72 -0
- package/test/fixtures/connectors/http-pull/.ontos/types.ts +61 -0
- package/test/fixtures/connectors/http-pull/package.json +5 -0
- package/test/fixtures/connectors/http-pull/streams/items/index.ts +37 -0
- package/test/fixtures/connectors/http-pull/streams/items/map.ts +12 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/case.json +15 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/exchanges.ndjson +2 -0
- package/test/fixtures/connectors/pg-pull/.ontos/descriptor.json +151 -0
- package/test/fixtures/connectors/pg-pull/.ontos/types.ts +62 -0
- package/test/fixtures/connectors/pg-pull/lib/sql.ts +14 -0
- package/test/fixtures/connectors/pg-pull/package.json +5 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/index.ts +30 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/case.json +15 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/exchanges.ndjson +3 -0
- package/test/fixtures/connectors/pg-pull/streams/orders/index.ts +32 -0
- package/test/fixtures/descriptor.ts +54 -0
- package/test/fixtures/kingdee-example.ts +82 -0
- package/test/fixtures/record-acceptance.ts +32 -0
- package/test/runtime/ctx.test.ts +434 -0
- package/test/runtime/helpers.ts +157 -0
- package/test/runtime/jobs.test.ts +444 -0
- package/test/runtime/operation.test.ts +175 -0
- package/test/runtime/session.test.ts +229 -0
- package/test/testing/acceptance-fixtures.test.ts +69 -0
- package/test/testing/compliance.test.ts +247 -0
- package/test/testing/engine.test.ts +283 -0
- package/test/testing/fixtures.test.ts +221 -0
- package/test/testing/gateway.test.ts +337 -0
- package/test/testing/import-guard.test.ts +45 -0
- package/test/testing/mock-external-system.test.ts +144 -0
- package/test/testing/operation-fixtures.test.ts +160 -0
- package/test/testing/recorder.test.ts +163 -0
- package/test/testing/redaction.test.ts +54 -0
- package/test/testing/replay.test.ts +207 -0
- package/test/testing/run-tests.test.ts +137 -0
- package/test/testing/source-loader.test.ts +220 -0
- package/test/validate-config.test.ts +104 -0
- package/tsconfig.build.json +23 -0
- package/tsconfig.json +19 -0
- package/cjs/index.cjs +0 -1015
- package/cjs/runtime.cjs +0 -752
- package/cjs/testing.cjs +0 -3591
- package/errors.d.ts +0 -17
- package/errors.js +0 -53
- package/guest.cjs +0 -2659
- package/index.d.ts +0 -13
- package/index.js +0 -7
- package/internal/location.d.ts +0 -6
- package/internal/location.js +0 -8
- package/presentation.d.ts +0 -38
- package/presentation.js +0 -268
- package/runtime/guest/bridge.d.ts +0 -53
- package/runtime/guest/bridge.js +0 -187
- package/runtime/guest/cache.d.ts +0 -3
- package/runtime/guest/cache.js +0 -42
- package/runtime/guest/crypto.d.ts +0 -13
- package/runtime/guest/crypto.js +0 -177
- package/runtime/guest/ctx.d.ts +0 -30
- package/runtime/guest/ctx.js +0 -124
- package/runtime/guest/db.d.ts +0 -6
- package/runtime/guest/db.js +0 -59
- package/runtime/guest/discover.d.ts +0 -29
- package/runtime/guest/discover.js +0 -51
- package/runtime/guest/failure.d.ts +0 -24
- package/runtime/guest/failure.js +0 -63
- package/runtime/guest/http.d.ts +0 -4
- package/runtime/guest/http.js +0 -204
- package/runtime/guest/index.d.ts +0 -12
- package/runtime/guest/index.js +0 -89
- package/runtime/guest/limits.js +0 -12
- package/runtime/guest/loader.d.ts +0 -15
- package/runtime/guest/loader.js +0 -42
- package/runtime/guest/log.d.ts +0 -10
- package/runtime/guest/log.js +0 -36
- package/runtime/guest/map.d.ts +0 -4
- package/runtime/guest/map.js +0 -45
- package/runtime/guest/paginate.d.ts +0 -6
- package/runtime/guest/paginate.js +0 -180
- package/runtime/guest/pull.d.ts +0 -12
- package/runtime/guest/pull.js +0 -150
- package/runtime/guest/push.d.ts +0 -18
- package/runtime/guest/push.js +0 -80
- package/runtime/guest/signal.d.ts +0 -3
- package/runtime/guest/signal.js +0 -12
- package/runtime/guest/time.d.ts +0 -5
- package/runtime/guest/time.js +0 -127
- package/runtime/guest-runtime.d.ts +0 -7
- package/runtime/guest-runtime.js +0 -51
- package/runtime/index.d.ts +0 -18
- package/runtime/index.js +0 -3
- package/runtime/protocol.d.ts +0 -276
- package/runtime/protocol.js +0 -409
- package/runtime/session.d.ts +0 -12
- package/runtime/session.js +0 -191
- package/t.d.ts +0 -18
- package/t.js +0 -26
- package/testing/child.cjs +0 -350
- package/testing/child.d.ts +0 -1
- package/testing/child.js +0 -12
- package/testing/compliance.d.ts +0 -41
- package/testing/compliance.js +0 -291
- package/testing/engine.d.ts +0 -66
- package/testing/engine.js +0 -339
- package/testing/fixtures.d.ts +0 -145
- package/testing/fixtures.js +0 -407
- package/testing/gateway.d.ts +0 -65
- package/testing/gateway.js +0 -402
- package/testing/import-guard.d.ts +0 -57
- package/testing/import-guard.js +0 -187
- package/testing/index.d.ts +0 -33
- package/testing/index.js +0 -10
- package/testing/local-db.d.ts +0 -27
- package/testing/local-db.js +0 -181
- package/testing/pinned-http.d.ts +0 -14
- package/testing/pinned-http.js +0 -125
- package/testing/recorder.d.ts +0 -15
- package/testing/recorder.js +0 -116
- package/testing/redaction.d.ts +0 -28
- package/testing/redaction.js +0 -67
- package/testing/replay.d.ts +0 -50
- package/testing/replay.js +0 -313
- package/testing/schema.d.ts +0 -13
- package/testing/schema.js +0 -65
- package/testing/source-loader.d.ts +0 -19
- package/testing/source-loader.js +0 -238
- package/testing/types.d.ts +0 -79
- package/testing/types.js +0 -18
- package/types.d.ts +0 -302
- package/types.js +0 -13
- package/validate.d.ts +0 -78
- package/validate.js +0 -597
package/testing/gateway.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import { createHash, createHmac, sign as rsaSign } from 'node:crypto';
|
|
2
|
-
import { lookup } from 'node:dns/promises';
|
|
3
|
-
import { createLocalDb, LocalDbError } from './local-db.js';
|
|
4
|
-
import { PinnedBodyTooLargeError, pinnedRequest } from './pinned-http.js';
|
|
5
|
-
import { redactSecretValues, secretRedactionTexts } from './redaction.js';
|
|
6
|
-
import { LocalRunError } from './types.js';
|
|
7
|
-
export class LocalGatewayError extends Error {
|
|
8
|
-
code;
|
|
9
|
-
status;
|
|
10
|
-
constructor(code, message, status) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'LocalGatewayError';
|
|
13
|
-
this.code = code;
|
|
14
|
-
this.status = status;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
18
|
-
const MAX_TIMEOUT_MS = 120_000;
|
|
19
|
-
const RESPONSE_MAX_BYTES = 10 * 1024 * 1024;
|
|
20
|
-
const MAX_REDIRECTS = 5;
|
|
21
|
-
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
22
|
-
const DROPPED_REQUEST_HEADERS = new Set(['host', 'content-length', 'connection', 'transfer-encoding', 'keep-alive', 'upgrade']);
|
|
23
|
-
const METADATA_ADDRESSES = new Set(['169.254.169.254', '100.100.100.200', 'fd00:ec2::254']);
|
|
24
|
-
const SECRET_FIELD = '[A-Za-z_][A-Za-z0-9_]*';
|
|
25
|
-
const PLAIN_HANDLE = new RegExp(`ontos:secret:(${SECRET_FIELD})`, 'g');
|
|
26
|
-
const URL_HANDLE = new RegExp(`ontos(?::|%3[Aa])secret(?::|%3[Aa])(${SECRET_FIELD})`, 'g');
|
|
27
|
-
function isRecord(value) {
|
|
28
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
|
-
}
|
|
30
|
-
function errorMessage(error) {
|
|
31
|
-
return error instanceof Error ? error.message : String(error);
|
|
32
|
-
}
|
|
33
|
-
function sleep(ms) {
|
|
34
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
-
}
|
|
36
|
-
export function normalizeHost(host) {
|
|
37
|
-
const value = host.trim().replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
38
|
-
const mapped = /^::ffff:(?:0:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(value);
|
|
39
|
-
if (mapped) {
|
|
40
|
-
const high = Number.parseInt(mapped[1] ?? '0', 16);
|
|
41
|
-
const low = Number.parseInt(mapped[2] ?? '0', 16);
|
|
42
|
-
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
43
|
-
}
|
|
44
|
-
const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(value);
|
|
45
|
-
if (dotted?.[1])
|
|
46
|
-
return dotted[1];
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
function matchHostPattern(host, pattern) {
|
|
50
|
-
const normalized = normalizeHost(pattern);
|
|
51
|
-
if (normalized.startsWith('*.')) {
|
|
52
|
-
const suffix = normalized.slice(1);
|
|
53
|
-
return suffix.length > 1 && host.length > suffix.length && host.endsWith(suffix);
|
|
54
|
-
}
|
|
55
|
-
return normalized !== '' && host === normalized;
|
|
56
|
-
}
|
|
57
|
-
function isMetadataAddress(host) {
|
|
58
|
-
return METADATA_ADDRESSES.has(normalizeHost(host));
|
|
59
|
-
}
|
|
60
|
-
function isIpLiteral(host) {
|
|
61
|
-
return /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(':');
|
|
62
|
-
}
|
|
63
|
-
function secretUnavailable(field) {
|
|
64
|
-
return new LocalGatewayError('EGRESS_SECRET_UNAVAILABLE', `Secret '${field}' is unavailable: it isn't configured locally`, 422);
|
|
65
|
-
}
|
|
66
|
-
function lookupSecret(values, field) {
|
|
67
|
-
const value = Object.hasOwn(values, field) ? values[field] : undefined;
|
|
68
|
-
if (typeof value !== 'string')
|
|
69
|
-
throw secretUnavailable(field);
|
|
70
|
-
return value;
|
|
71
|
-
}
|
|
72
|
-
function substituteHandles(text, values) {
|
|
73
|
-
for (const match of text.matchAll(PLAIN_HANDLE))
|
|
74
|
-
lookupSecret(values, match[1] ?? '');
|
|
75
|
-
return text.replace(PLAIN_HANDLE, (_match, field) => lookupSecret(values, field));
|
|
76
|
-
}
|
|
77
|
-
function substituteUrlHandles(text, values) {
|
|
78
|
-
for (const match of text.matchAll(URL_HANDLE))
|
|
79
|
-
lookupSecret(values, match[1] ?? '');
|
|
80
|
-
return text.replace(URL_HANDLE, (_match, field) => encodeURIComponent(lookupSecret(values, field)));
|
|
81
|
-
}
|
|
82
|
-
export class LocalLimiter {
|
|
83
|
-
perSecond;
|
|
84
|
-
concurrency;
|
|
85
|
-
nextAt = 0;
|
|
86
|
-
active = 0;
|
|
87
|
-
waiting = [];
|
|
88
|
-
constructor(perSecond, concurrency) {
|
|
89
|
-
this.perSecond = perSecond;
|
|
90
|
-
this.concurrency = concurrency;
|
|
91
|
-
}
|
|
92
|
-
async acquire() {
|
|
93
|
-
if (this.active < this.concurrency && this.waiting.length === 0) {
|
|
94
|
-
this.active += 1;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
await new Promise((resolve) => this.waiting.push(resolve));
|
|
98
|
-
}
|
|
99
|
-
const now = Date.now();
|
|
100
|
-
const at = Math.max(now, this.nextAt);
|
|
101
|
-
this.nextAt = at + 1000 / this.perSecond;
|
|
102
|
-
if (at > now)
|
|
103
|
-
await sleep(at - now);
|
|
104
|
-
let released = false;
|
|
105
|
-
return () => {
|
|
106
|
-
if (released)
|
|
107
|
-
return;
|
|
108
|
-
released = true;
|
|
109
|
-
const next = this.waiting.shift();
|
|
110
|
-
if (next)
|
|
111
|
-
next();
|
|
112
|
-
else
|
|
113
|
-
this.active -= 1;
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function parseHttpRequest(payload) {
|
|
118
|
-
if (!isRecord(payload) || typeof payload.url !== 'string' || typeof payload.method !== 'string') {
|
|
119
|
-
throw new LocalGatewayError('BAD_REQUEST', 'http request must have a method and a url', 400);
|
|
120
|
-
}
|
|
121
|
-
return payload;
|
|
122
|
-
}
|
|
123
|
-
function scriptUrlOf(request) {
|
|
124
|
-
let url;
|
|
125
|
-
try {
|
|
126
|
-
url = new URL(request.url);
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
throw new LocalGatewayError('BAD_REQUEST', 'url must be an absolute URL', 400);
|
|
130
|
-
}
|
|
131
|
-
for (const [name, value] of Object.entries(request.query ?? {})) {
|
|
132
|
-
for (const item of Array.isArray(value) ? value : [value])
|
|
133
|
-
url.searchParams.append(name, item);
|
|
134
|
-
}
|
|
135
|
-
return url;
|
|
136
|
-
}
|
|
137
|
-
const utf8 = new TextDecoder('utf-8', { fatal: true });
|
|
138
|
-
function encodeBody(bytes) {
|
|
139
|
-
try {
|
|
140
|
-
return { body: utf8.decode(bytes), bodyEncoding: 'utf8' };
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return { body: Buffer.from(bytes).toString('base64'), bodyEncoding: 'base64' };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function tooLarge() {
|
|
147
|
-
return new LocalGatewayError('EGRESS_RESPONSE_TOO_LARGE', `The external system's response exceeds the ${RESPONSE_MAX_BYTES / 1024 / 1024} MiB limit`, 502);
|
|
148
|
-
}
|
|
149
|
-
function hostDenied(host) {
|
|
150
|
-
return new LocalRunError('HOST_DENIED', `Access to host '${host}' is denied: add it to the connector's hosts and the local configuration`);
|
|
151
|
-
}
|
|
152
|
-
function computeCrypto(request, values) {
|
|
153
|
-
const data = substituteHandles(request.data, values);
|
|
154
|
-
switch (request.op) {
|
|
155
|
-
case 'hash':
|
|
156
|
-
return { value: createHash(request.algorithm).update(data, 'utf8').digest(request.encoding) };
|
|
157
|
-
case 'hmac':
|
|
158
|
-
return { value: createHmac(request.algorithm, substituteHandles(request.key, values)).update(data, 'utf8').digest(request.encoding) };
|
|
159
|
-
case 'sign': {
|
|
160
|
-
try {
|
|
161
|
-
return { value: rsaSign('sha256', Buffer.from(data, 'utf8'), substituteHandles(request.key, values)).toString(request.encoding) };
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
if (error instanceof LocalGatewayError)
|
|
165
|
-
throw error;
|
|
166
|
-
throw new LocalGatewayError('BAD_REQUEST', 'key is not a valid RSA private key', 400);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
default:
|
|
170
|
-
throw new LocalGatewayError('BAD_REQUEST', 'op must be one of hmac, sign, hash', 400);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
export function createLocalGateway(options) {
|
|
174
|
-
const secrets = { ...options.secrets };
|
|
175
|
-
const secretTexts = secretRedactionTexts([...Object.values(secrets), ...(options.db?.password ? [options.db.password] : [])]);
|
|
176
|
-
const redact = (text) => redactSecretValues(text, secretTexts);
|
|
177
|
-
const hostPatterns = [...options.hosts];
|
|
178
|
-
const doFetch = options.fetch ?? fetch;
|
|
179
|
-
const realNetwork = options.fetch === undefined;
|
|
180
|
-
const resolveHost = options.resolve ?? ((host) => lookup(host, { all: true, verbatim: true }));
|
|
181
|
-
const limiter = options.rateLimit && options.rateLimit.perSecond > 0
|
|
182
|
-
? new LocalLimiter(options.rateLimit.perSecond, Math.max(1, Math.floor(options.rateLimit.concurrency || 1)))
|
|
183
|
-
: null;
|
|
184
|
-
const cache = new Map();
|
|
185
|
-
let db = null;
|
|
186
|
-
const exchange = (entry) => {
|
|
187
|
-
try {
|
|
188
|
-
options.onExchange?.(entry);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
const checkHost = async (url) => {
|
|
194
|
-
const host = normalizeHost(url.hostname);
|
|
195
|
-
if (isMetadataAddress(host))
|
|
196
|
-
throw hostDenied(host);
|
|
197
|
-
const allowed = hostPatterns.some((pattern) => matchHostPattern(host, pattern) || normalizeHost(pattern) === url.host.toLowerCase());
|
|
198
|
-
if (!allowed)
|
|
199
|
-
throw hostDenied(host);
|
|
200
|
-
if (!realNetwork)
|
|
201
|
-
return null;
|
|
202
|
-
if (isIpLiteral(host))
|
|
203
|
-
return [{ address: host, family: host.includes(':') ? 6 : 4 }];
|
|
204
|
-
let addresses;
|
|
205
|
-
try {
|
|
206
|
-
addresses = await resolveHost(host);
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
throw new LocalGatewayError('EGRESS_UPSTREAM_FAILED', redact(`The outbound request failed: cannot resolve ${host}: ${errorMessage(error)}`), 502);
|
|
210
|
-
}
|
|
211
|
-
if (addresses.length === 0)
|
|
212
|
-
throw new LocalGatewayError('EGRESS_UPSTREAM_FAILED', `The outbound request failed: cannot resolve ${host}`, 502);
|
|
213
|
-
if (addresses.some((record) => isMetadataAddress(record.address)))
|
|
214
|
-
throw hostDenied(host);
|
|
215
|
-
return addresses;
|
|
216
|
-
};
|
|
217
|
-
const sendOnce = async (url, method, headers, body, addresses, signal) => {
|
|
218
|
-
try {
|
|
219
|
-
if (addresses !== null)
|
|
220
|
-
return await pinnedRequest({ url, method, headers, body, addresses, signal, maxBytes: RESPONSE_MAX_BYTES });
|
|
221
|
-
return await doFetch(url.toString(), {
|
|
222
|
-
method,
|
|
223
|
-
headers,
|
|
224
|
-
...(body !== null && method !== 'GET' && method !== 'HEAD' ? { body: body } : {}),
|
|
225
|
-
redirect: 'manual',
|
|
226
|
-
signal,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
if (error instanceof PinnedBodyTooLargeError)
|
|
231
|
-
throw tooLarge();
|
|
232
|
-
if (signal.aborted)
|
|
233
|
-
throw new LocalGatewayError('EGRESS_TIMEOUT', 'The outbound request timed out', 504);
|
|
234
|
-
throw new LocalGatewayError('EGRESS_UPSTREAM_FAILED', redact(`The outbound request failed: ${errorMessage(error)}`), 502);
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
const http = async (payload) => {
|
|
238
|
-
const request = parseHttpRequest(payload);
|
|
239
|
-
const scriptUrl = scriptUrlOf(request);
|
|
240
|
-
let url;
|
|
241
|
-
try {
|
|
242
|
-
url = new URL(substituteUrlHandles(scriptUrl.toString(), secrets));
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
if (error instanceof LocalGatewayError)
|
|
246
|
-
throw error;
|
|
247
|
-
throw new LocalGatewayError('BAD_REQUEST', 'url is not valid after substituting secrets', 400);
|
|
248
|
-
}
|
|
249
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
250
|
-
throw new LocalGatewayError('EGRESS_SCHEME_DENIED', 'Only http(s) outbound requests are allowed', 403);
|
|
251
|
-
let addresses = await checkHost(url);
|
|
252
|
-
let headers = {};
|
|
253
|
-
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
254
|
-
if (DROPPED_REQUEST_HEADERS.has(name.toLowerCase()))
|
|
255
|
-
continue;
|
|
256
|
-
headers[name] = substituteHandles(value, secrets);
|
|
257
|
-
}
|
|
258
|
-
let body = null;
|
|
259
|
-
if (request.body !== null && request.body !== undefined) {
|
|
260
|
-
body = request.bodyEncoding === 'base64' ? Buffer.from(request.body, 'base64') : Buffer.from(substituteHandles(request.body, secrets), 'utf8');
|
|
261
|
-
}
|
|
262
|
-
const timeoutMs = Math.min(request.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
263
|
-
const release = limiter ? await limiter.acquire() : () => undefined;
|
|
264
|
-
const controller = new AbortController();
|
|
265
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
266
|
-
try {
|
|
267
|
-
let method = request.method;
|
|
268
|
-
let redirects = 0;
|
|
269
|
-
for (;;) {
|
|
270
|
-
const response = await sendOnce(url, method, headers, body, addresses, controller.signal);
|
|
271
|
-
const location = response.headers.get('location');
|
|
272
|
-
if (REDIRECT_STATUSES.has(response.status) && location !== null && redirects < MAX_REDIRECTS) {
|
|
273
|
-
let next = null;
|
|
274
|
-
try {
|
|
275
|
-
next = new URL(location, url);
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
next = null;
|
|
279
|
-
}
|
|
280
|
-
if (next && next.host === url.host && (next.protocol === 'https:' || next.protocol === 'http:')) {
|
|
281
|
-
redirects += 1;
|
|
282
|
-
if (response.status === 303 ? method !== 'HEAD' : (response.status === 301 || response.status === 302) && method !== 'GET' && method !== 'HEAD') {
|
|
283
|
-
method = 'GET';
|
|
284
|
-
body = null;
|
|
285
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== 'content-type'));
|
|
286
|
-
}
|
|
287
|
-
url = next;
|
|
288
|
-
addresses = await checkHost(url);
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
let bytes;
|
|
293
|
-
try {
|
|
294
|
-
bytes = method === 'HEAD' ? new Uint8Array() : new Uint8Array(await response.arrayBuffer());
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
if (controller.signal.aborted)
|
|
298
|
-
throw new LocalGatewayError('EGRESS_TIMEOUT', 'The outbound request timed out', 504);
|
|
299
|
-
throw new LocalGatewayError('EGRESS_UPSTREAM_FAILED', redact(`Reading the response failed: ${errorMessage(error)}`), 502);
|
|
300
|
-
}
|
|
301
|
-
if (bytes.byteLength > RESPONSE_MAX_BYTES)
|
|
302
|
-
throw tooLarge();
|
|
303
|
-
const responseHeaders = {};
|
|
304
|
-
response.headers.forEach((value, name) => {
|
|
305
|
-
const lower = name.toLowerCase();
|
|
306
|
-
if (lower === 'content-encoding' || lower === 'content-length')
|
|
307
|
-
return;
|
|
308
|
-
responseHeaders[lower] = value;
|
|
309
|
-
});
|
|
310
|
-
return {
|
|
311
|
-
status: response.status,
|
|
312
|
-
url: redirects === 0 ? scriptUrl.toString() : url.toString(),
|
|
313
|
-
headers: responseHeaders,
|
|
314
|
-
...encodeBody(bytes),
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
finally {
|
|
319
|
-
clearTimeout(timer);
|
|
320
|
-
release();
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
const redactResponse = (response) => ({
|
|
324
|
-
...response,
|
|
325
|
-
url: redact(response.url),
|
|
326
|
-
headers: Object.fromEntries(Object.entries(response.headers).map(([name, value]) => [name, redact(value)])),
|
|
327
|
-
body: response.bodyEncoding === 'utf8' ? redact(response.body) : response.body,
|
|
328
|
-
});
|
|
329
|
-
const dbOf = () => {
|
|
330
|
-
if (!options.db)
|
|
331
|
-
throw new LocalGatewayError('DB_PROXY_NOT_DECLARED', 'ctx.db is not configured locally: set db in the local configuration', 403);
|
|
332
|
-
if (isMetadataAddress(options.db.host))
|
|
333
|
-
throw hostDenied(normalizeHost(options.db.host));
|
|
334
|
-
db ??= createLocalDb(options.db, { driver: options.postgres, secrets: Object.values(secrets) });
|
|
335
|
-
return db;
|
|
336
|
-
};
|
|
337
|
-
const traced = async (kind, request, run) => {
|
|
338
|
-
const startedAt = Date.now();
|
|
339
|
-
try {
|
|
340
|
-
const response = await run();
|
|
341
|
-
exchange({ kind, request, response, error: null, durationMs: Date.now() - startedAt });
|
|
342
|
-
return response;
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
const code = isRecord(error) && typeof error.code === 'string' ? error.code : 'HOST_CALL_FAILED';
|
|
346
|
-
exchange({ kind, request, response: null, error: { code, message: errorMessage(error) }, durationMs: Date.now() - startedAt });
|
|
347
|
-
throw error;
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
const call = async (method, payload) => {
|
|
351
|
-
switch (method) {
|
|
352
|
-
case 'http':
|
|
353
|
-
return redactResponse(await traced('http', payload, () => http(payload)));
|
|
354
|
-
case 'db.query': {
|
|
355
|
-
const request = payload;
|
|
356
|
-
return traced('db', request, () => dbOf().query({ sql: request.sql, params: request.params ?? [] }));
|
|
357
|
-
}
|
|
358
|
-
case 'db.next': {
|
|
359
|
-
const cursorId = isRecord(payload) && typeof payload.cursorId === 'string' ? payload.cursorId : '';
|
|
360
|
-
return traced('db', { cursorId }, () => dbOf().next(cursorId));
|
|
361
|
-
}
|
|
362
|
-
case 'db.close': {
|
|
363
|
-
const cursorId = isRecord(payload) && typeof payload.cursorId === 'string' ? payload.cursorId : '';
|
|
364
|
-
await dbOf().close(cursorId);
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
case 'crypto':
|
|
368
|
-
return traced('crypto', payload, async () => computeCrypto(payload, secrets));
|
|
369
|
-
case 'cache.get': {
|
|
370
|
-
const key = isRecord(payload) && typeof payload.key === 'string' ? payload.key : '';
|
|
371
|
-
const entry = cache.get(key);
|
|
372
|
-
if (!entry || Date.parse(entry.expiresAt) <= Date.now())
|
|
373
|
-
return null;
|
|
374
|
-
return entry;
|
|
375
|
-
}
|
|
376
|
-
case 'cache.set': {
|
|
377
|
-
if (!isRecord(payload) || typeof payload.key !== 'string' || typeof payload.value !== 'string' || typeof payload.ttlSeconds !== 'number') {
|
|
378
|
-
throw new LocalGatewayError('BAD_REQUEST', 'cache.set needs key, value and ttlSeconds', 400);
|
|
379
|
-
}
|
|
380
|
-
cache.set(payload.key, { value: payload.value, expiresAt: new Date(Date.now() + payload.ttlSeconds * 1000).toISOString() });
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
case 'secret.reveal': {
|
|
384
|
-
const field = isRecord(payload) && typeof payload.field === 'string' ? payload.field : '';
|
|
385
|
-
return { value: lookupSecret(secrets, field) };
|
|
386
|
-
}
|
|
387
|
-
default:
|
|
388
|
-
throw new LocalGatewayError('HOST_METHOD_UNSUPPORTED', `the local gateway does not answer ${method}`);
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
return {
|
|
392
|
-
call,
|
|
393
|
-
redact,
|
|
394
|
-
async close() {
|
|
395
|
-
const current = db;
|
|
396
|
-
db = null;
|
|
397
|
-
if (current)
|
|
398
|
-
await current.end();
|
|
399
|
-
},
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
export { LocalDbError };
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 本地源码加载器的导入检查(不执行任何代码,只用调用方传入的 TypeScript 编译器 API 解析语法树)。
|
|
3
|
-
*
|
|
4
|
-
* 两层:
|
|
5
|
-
* 1. `findForbiddenSourceSyntax`:与 shared `server/connector-source-guard.ts` 的同名函数规则相同(SDK 不依赖 shared,
|
|
6
|
-
* 这里另写一份,`test/testing/import-guard.test.ts` 用同一批样例对照两份实现):
|
|
7
|
-
* - 任何 import 属性 / 断言(静态 `import`、`export … from` 的 `with` / `assert`,动态 `import(x, options)` 的第二参);
|
|
8
|
-
* - `macro:` 开头的说明符;
|
|
9
|
-
* - SDK 只认精确的 `@deepexi/datasense-connector`:子路径(含 `/testing`)与改名前的旧包名都拒绝;
|
|
10
|
-
* - TypeScript 解析出的语法错误。
|
|
11
|
-
* 2. `classifySpecifier`:转译后代码里每个 `require(…)` 的说明符(与服务端打包插件的白名单相同):只允许相对路径与
|
|
12
|
-
* `@deepexi/datasense-connector`;`node:*` 与 Node 内置模块、裸包名、绝对路径与 URL 说明符都拒绝;逃出源码根的相对路径
|
|
13
|
-
* 单独报 `PATH_ESCAPE`。
|
|
14
|
-
*/
|
|
15
|
-
import type * as TS from 'typescript';
|
|
16
|
-
export type TypeScriptApi = typeof TS;
|
|
17
|
-
export interface SourceGuardDiagnostic {
|
|
18
|
-
/** 相对源码根目录的路径。 */
|
|
19
|
-
file: string;
|
|
20
|
-
/** 从 1 开始。 */
|
|
21
|
-
line: number;
|
|
22
|
-
/** 从 0 开始。 */
|
|
23
|
-
column: number;
|
|
24
|
-
message: string;
|
|
25
|
-
/** `syntax`:语法错误;`import`:禁用的导入写法。 */
|
|
26
|
-
kind: 'syntax' | 'import';
|
|
27
|
-
}
|
|
28
|
-
export declare const CONNECTOR_SDK_SPECIFIER = "@deepexi/datasense-connector";
|
|
29
|
-
export declare function asTypeScript(value: unknown): TypeScriptApi;
|
|
30
|
-
/** 检查一个源码文件,返回全部违规(空数组表示通过)。规则与 shared 的 `findForbiddenSourceSyntax` 相同。 */
|
|
31
|
-
export declare function findForbiddenSourceSyntax(ts: TypeScriptApi, filePath: string, content: string): SourceGuardDiagnostic[];
|
|
32
|
-
/** 转译后 CommonJS 代码里的 `require(…)`:`specifier` 为 null 表示参数不是字面量(运行时才知道导入什么)。 */
|
|
33
|
-
export declare function collectRequires(ts: TypeScriptApi, filePath: string, output: string): Array<{
|
|
34
|
-
specifier: string | null;
|
|
35
|
-
line: number;
|
|
36
|
-
}>;
|
|
37
|
-
/**
|
|
38
|
-
* 源码里写出的值导入说明符(`import` / `export … from` / `import()` / `require()` 的字面量;`import type` 与 `export type` 不算)。
|
|
39
|
-
* 转译会删掉没用到的导入,只看转译结果会放过 `import fs from 'node:fs'` 这类写了没用的导入,所以源码这一层也要归类。
|
|
40
|
-
*/
|
|
41
|
-
export declare function collectSourceSpecifiers(ts: TypeScriptApi, filePath: string, content: string): Array<{
|
|
42
|
-
specifier: string;
|
|
43
|
-
line: number;
|
|
44
|
-
}>;
|
|
45
|
-
export type SpecifierClass = {
|
|
46
|
-
kind: 'sdk';
|
|
47
|
-
} | {
|
|
48
|
-
kind: 'relative';
|
|
49
|
-
target: string;
|
|
50
|
-
} | {
|
|
51
|
-
kind: 'escape';
|
|
52
|
-
} | {
|
|
53
|
-
kind: 'forbidden';
|
|
54
|
-
message: string;
|
|
55
|
-
};
|
|
56
|
-
/** 一个 `require` 说明符的归类;`importer` 是相对源码根目录的路径。 */
|
|
57
|
-
export declare function classifySpecifier(specifier: string, importer: string): SpecifierClass;
|
package/testing/import-guard.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
export const CONNECTOR_SDK_SPECIFIER = '@deepexi/datasense-connector';
|
|
2
|
-
const RETIRED_SDK_SPECIFIER = ['@mu-ontos', 'connector-sdk'].join('/');
|
|
3
|
-
const MACRO_SPECIFIER = /^macro:/i;
|
|
4
|
-
const NODE_BUILTINS = new Set([
|
|
5
|
-
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel',
|
|
6
|
-
'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
|
|
7
|
-
'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events', 'tty', 'url',
|
|
8
|
-
'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib', 'test', 'sqlite', 'sea',
|
|
9
|
-
]);
|
|
10
|
-
export function asTypeScript(value) {
|
|
11
|
-
const api = (value !== null && typeof value === 'object' && 'default' in value && typeof value.default === 'object'
|
|
12
|
-
? value.default
|
|
13
|
-
: value);
|
|
14
|
-
if (!api || typeof api.createSourceFile !== 'function' || typeof api.transpileModule !== 'function') {
|
|
15
|
-
throw new TypeError('options.typescript must be the TypeScript compiler API (import ts from "typescript")');
|
|
16
|
-
}
|
|
17
|
-
return api;
|
|
18
|
-
}
|
|
19
|
-
function scriptKindOf(ts, filePath) {
|
|
20
|
-
if (filePath.endsWith('.tsx'))
|
|
21
|
-
return ts.ScriptKind.TSX;
|
|
22
|
-
if (filePath.endsWith('.jsx'))
|
|
23
|
-
return ts.ScriptKind.JSX;
|
|
24
|
-
if (/\.[cm]?js$/.test(filePath))
|
|
25
|
-
return ts.ScriptKind.JS;
|
|
26
|
-
return ts.ScriptKind.TS;
|
|
27
|
-
}
|
|
28
|
-
function sdkSpecifierProblem(specifier) {
|
|
29
|
-
if (specifier === RETIRED_SDK_SPECIFIER || specifier.startsWith(`${RETIRED_SDK_SPECIFIER}/`)) {
|
|
30
|
-
return `import '${specifier}' is not allowed; the SDK package is now '${CONNECTOR_SDK_SPECIFIER}'`;
|
|
31
|
-
}
|
|
32
|
-
if (specifier.startsWith(`${CONNECTOR_SDK_SPECIFIER}/`)) {
|
|
33
|
-
return `import '${specifier}' is not allowed; import '${CONNECTOR_SDK_SPECIFIER}' itself`;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
function literalSpecifier(ts, node) {
|
|
38
|
-
if (node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)))
|
|
39
|
-
return node.text;
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
export function findForbiddenSourceSyntax(ts, filePath, content) {
|
|
43
|
-
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindOf(ts, filePath));
|
|
44
|
-
const problems = [];
|
|
45
|
-
const report = (position, message, kind) => {
|
|
46
|
-
const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
|
|
47
|
-
problems.push({ file: filePath, line: line + 1, column: character, message, kind });
|
|
48
|
-
};
|
|
49
|
-
const parseDiagnostics = sourceFile.parseDiagnostics ?? [];
|
|
50
|
-
for (const diagnostic of parseDiagnostics) {
|
|
51
|
-
report(diagnostic.start ?? 0, `syntax error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ')}`, 'syntax');
|
|
52
|
-
}
|
|
53
|
-
const checkSpecifier = (node) => {
|
|
54
|
-
const specifier = literalSpecifier(ts, node);
|
|
55
|
-
if (!node || specifier === null)
|
|
56
|
-
return;
|
|
57
|
-
if (MACRO_SPECIFIER.test(specifier)) {
|
|
58
|
-
report(node.getStart(sourceFile), `import '${specifier}' is not allowed; connectors cannot use Bun macros`, 'import');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const sdkProblem = sdkSpecifierProblem(specifier);
|
|
62
|
-
if (sdkProblem !== null)
|
|
63
|
-
report(node.getStart(sourceFile), sdkProblem, 'import');
|
|
64
|
-
};
|
|
65
|
-
const attributesMessage = (keyword) => `import attributes ('${keyword} { … }') are not allowed; connectors cannot use Bun macros or other import attributes`;
|
|
66
|
-
const visit = (node) => {
|
|
67
|
-
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
68
|
-
checkSpecifier(node.moduleSpecifier);
|
|
69
|
-
if (node.attributes) {
|
|
70
|
-
report(node.attributes.getStart(sourceFile), attributesMessage(node.attributes.token === ts.SyntaxKind.AssertKeyword ? 'assert' : 'with'), 'import');
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
else if (ts.isImportEqualsDeclaration(node)) {
|
|
74
|
-
if (ts.isExternalModuleReference(node.moduleReference))
|
|
75
|
-
checkSpecifier(node.moduleReference.expression);
|
|
76
|
-
}
|
|
77
|
-
else if (ts.isCallExpression(node)) {
|
|
78
|
-
const callee = node.expression;
|
|
79
|
-
if (callee.kind === ts.SyntaxKind.ImportKeyword) {
|
|
80
|
-
checkSpecifier(node.arguments[0]);
|
|
81
|
-
const options = node.arguments[1];
|
|
82
|
-
if (options)
|
|
83
|
-
report(options.getStart(sourceFile), attributesMessage('with'), 'import');
|
|
84
|
-
}
|
|
85
|
-
else if (ts.isIdentifier(callee) && callee.text === 'require') {
|
|
86
|
-
checkSpecifier(node.arguments[0]);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
ts.forEachChild(node, visit);
|
|
90
|
-
};
|
|
91
|
-
visit(sourceFile);
|
|
92
|
-
return problems;
|
|
93
|
-
}
|
|
94
|
-
export function collectRequires(ts, filePath, output) {
|
|
95
|
-
const sourceFile = ts.createSourceFile(`${filePath}.js`, output, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
96
|
-
const found = [];
|
|
97
|
-
const visit = (node) => {
|
|
98
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
99
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
100
|
-
found.push({ specifier: literalSpecifier(ts, node.arguments[0]), line: line + 1 });
|
|
101
|
-
}
|
|
102
|
-
ts.forEachChild(node, visit);
|
|
103
|
-
};
|
|
104
|
-
visit(sourceFile);
|
|
105
|
-
return found;
|
|
106
|
-
}
|
|
107
|
-
export function collectSourceSpecifiers(ts, filePath, content) {
|
|
108
|
-
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindOf(ts, filePath));
|
|
109
|
-
const found = [];
|
|
110
|
-
const add = (node) => {
|
|
111
|
-
const specifier = literalSpecifier(ts, node);
|
|
112
|
-
if (!node || specifier === null)
|
|
113
|
-
return;
|
|
114
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
115
|
-
found.push({ specifier, line: line + 1 });
|
|
116
|
-
};
|
|
117
|
-
const visit = (node) => {
|
|
118
|
-
if (ts.isImportDeclaration(node)) {
|
|
119
|
-
if (!node.importClause?.isTypeOnly)
|
|
120
|
-
add(node.moduleSpecifier);
|
|
121
|
-
}
|
|
122
|
-
else if (ts.isExportDeclaration(node)) {
|
|
123
|
-
if (!node.isTypeOnly)
|
|
124
|
-
add(node.moduleSpecifier);
|
|
125
|
-
}
|
|
126
|
-
else if (ts.isImportEqualsDeclaration(node)) {
|
|
127
|
-
if (!node.isTypeOnly && ts.isExternalModuleReference(node.moduleReference))
|
|
128
|
-
add(node.moduleReference.expression);
|
|
129
|
-
}
|
|
130
|
-
else if (ts.isCallExpression(node)) {
|
|
131
|
-
const callee = node.expression;
|
|
132
|
-
if (callee.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(callee) && callee.text === 'require'))
|
|
133
|
-
add(node.arguments[0]);
|
|
134
|
-
}
|
|
135
|
-
ts.forEachChild(node, visit);
|
|
136
|
-
};
|
|
137
|
-
visit(sourceFile);
|
|
138
|
-
return found;
|
|
139
|
-
}
|
|
140
|
-
function packageNameOf(specifier) {
|
|
141
|
-
const parts = specifier.split('/');
|
|
142
|
-
if (specifier.startsWith('@'))
|
|
143
|
-
return parts.slice(0, 2).join('/');
|
|
144
|
-
return parts[0] ?? specifier;
|
|
145
|
-
}
|
|
146
|
-
function normalizePosix(input) {
|
|
147
|
-
const out = [];
|
|
148
|
-
for (const segment of input.split('/')) {
|
|
149
|
-
if (segment === '' || segment === '.')
|
|
150
|
-
continue;
|
|
151
|
-
if (segment === '..') {
|
|
152
|
-
if (out.length > 0 && out[out.length - 1] !== '..')
|
|
153
|
-
out.pop();
|
|
154
|
-
else
|
|
155
|
-
out.push('..');
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
out.push(segment);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return out.length === 0 ? '.' : out.join('/');
|
|
162
|
-
}
|
|
163
|
-
export function classifySpecifier(specifier, importer) {
|
|
164
|
-
if (specifier === CONNECTOR_SDK_SPECIFIER)
|
|
165
|
-
return { kind: 'sdk' };
|
|
166
|
-
const isRelative = specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../');
|
|
167
|
-
if (isRelative) {
|
|
168
|
-
const slash = importer.lastIndexOf('/');
|
|
169
|
-
const baseDir = slash === -1 ? '' : importer.slice(0, slash);
|
|
170
|
-
const target = normalizePosix(baseDir ? `${baseDir}/${specifier}` : specifier);
|
|
171
|
-
if (target === '..' || target.startsWith('../'))
|
|
172
|
-
return { kind: 'escape' };
|
|
173
|
-
return { kind: 'relative', target };
|
|
174
|
-
}
|
|
175
|
-
const sdkProblem = sdkSpecifierProblem(specifier);
|
|
176
|
-
if (sdkProblem !== null)
|
|
177
|
-
return { kind: 'forbidden', message: sdkProblem };
|
|
178
|
-
if (MACRO_SPECIFIER.test(specifier))
|
|
179
|
-
return { kind: 'forbidden', message: `import '${specifier}' is not allowed; connectors cannot use Bun macros` };
|
|
180
|
-
if (specifier.startsWith('node:') || specifier.startsWith('bun:') || specifier === 'bun' || NODE_BUILTINS.has(packageNameOf(specifier))) {
|
|
181
|
-
return { kind: 'forbidden', message: `import '${specifier}' is not allowed; connectors cannot use Node.js or Bun built-in modules` };
|
|
182
|
-
}
|
|
183
|
-
if (specifier.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(specifier)) {
|
|
184
|
-
return { kind: 'forbidden', message: `import '${specifier}' is not allowed; only relative imports and '${CONNECTOR_SDK_SPECIFIER}' are allowed` };
|
|
185
|
-
}
|
|
186
|
-
return { kind: 'forbidden', message: `import '${specifier}' is not allowed; connectors can only depend on '${CONNECTOR_SDK_SPECIFIER}'` };
|
|
187
|
-
}
|