@zanii/gateway 0.1.2 → 0.1.3

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 CHANGED
@@ -38,6 +38,7 @@ API key from `ZANII_API_KEY`.
38
38
 
39
39
  ## Changelog
40
40
 
41
+ - **0.1.3** — `createAdvancedHandler` with opt-in controls: salted request / response body commitments, an egress path allow-list, PII type detection (`detectPiiTypes`; values never recorded), per-call cost, a response-integrity hash, per-route rate limits and idempotency-key replay protection. Refused calls never reach the upstream and are still receipted.
41
42
  - **0.1.2** — now on `@zanii/sdk` 0.5: request receipts commit their payload as RFC 8785 bytes, so they verify from Python and TypeScript alike, and carry the ledger-outage and chain fixes. No API change.
42
43
  - **0.1.0** — initial release: `createZaniiHandler` (fetch), `createZaniiGateway` (Node), `zanii-gateway` CLI.
43
44
 
@@ -0,0 +1,36 @@
1
+ import type { GatewayOptions } from './index.js';
2
+ export interface AdvancedOptions extends GatewayOptions {
3
+ /** Commit request and/or response bodies with a fresh salt. `onNonce` receives each salt to keep. */
4
+ bodyCommitment?: {
5
+ request?: boolean;
6
+ response?: boolean;
7
+ onNonce?: (target: string, kind: 'request' | 'response', nonce: string) => void;
8
+ };
9
+ /** Only these path prefixes may be forwarded. */
10
+ allowPaths?: string[];
11
+ /** Flag personal data in request bodies (types only are recorded, never the values). */
12
+ detectPii?: boolean;
13
+ /** Price a call (integer micro-units). */
14
+ cost?: (call: {
15
+ method: string;
16
+ path: string;
17
+ status: number;
18
+ }) => {
19
+ cost_micro: number;
20
+ currency: string;
21
+ } | undefined;
22
+ /** Hash the response body into the receipt (proves what the upstream returned). */
23
+ responseHash?: boolean;
24
+ /** Calls per window per path prefix. */
25
+ rateLimits?: Record<string, {
26
+ max: number;
27
+ windowMs: number;
28
+ }>;
29
+ /** Refuse a repeated `idempotency-key` within the window (stops double payments). */
30
+ replayWindowMs?: number;
31
+ now?: () => number;
32
+ }
33
+ /** Which kinds of personal data appear in a text (the values are never returned). */
34
+ export declare function detectPiiTypes(text: string): string[];
35
+ export declare function createAdvancedHandler(opts: AdvancedOptions): (request: Request) => Promise<Response>;
36
+ //# sourceMappingURL=advanced.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,qGAAqG;IACrG,cAAc,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IAC5I,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACxH,mFAAmF;IACnF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAqBD,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKrD;AASD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAqEpG"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * A gateway with more controls, all opt-in: salted body commitments, an egress allow-list,
3
+ * PII detection, per-call cost, a response-integrity hash, rate limits per route, and
4
+ * replay protection on idempotency keys. Refused calls never reach the upstream and are
5
+ * still receipted. `createZaniiHandler` itself is unchanged.
6
+ */
7
+ import { createHash, randomBytes } from 'node:crypto';
8
+ const PII = [
9
+ ['email', /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i],
10
+ ['phone', /(?:\+|00)\d[\d\s-]{7,14}\d/],
11
+ ['emirates-id', /\b784-?\d{4}-?\d{7}-?\d\b/],
12
+ ['iban', /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/],
13
+ ];
14
+ function luhn(digits) {
15
+ let sum = 0;
16
+ for (let i = 0; i < digits.length; i++) {
17
+ let d = Number(digits[digits.length - 1 - i]);
18
+ if (i % 2)
19
+ d = d * 2 > 9 ? d * 2 - 9 : d * 2;
20
+ sum += d;
21
+ }
22
+ return sum % 10 === 0;
23
+ }
24
+ /** Which kinds of personal data appear in a text (the values are never returned). */
25
+ export function detectPiiTypes(text) {
26
+ const found = PII.filter(([, re]) => re.test(text)).map(([t]) => t);
27
+ const cards = text.match(/\b(?:\d[ -]?){13,19}\b/g) ?? [];
28
+ if (cards.some((c) => luhn(c.replace(/\D/g, ''))))
29
+ found.push('payment-card');
30
+ return found;
31
+ }
32
+ const sha = (b) => 'sha256:' + createHash('sha256').update(b).digest('hex');
33
+ function targetFor(prefix, method, pathname) {
34
+ const path = pathname.replace(/[^a-z0-9]+/gi, '.').replace(/^\.|\.$/g, '') || 'root';
35
+ return `${prefix}.${method.toLowerCase()}.${path}`;
36
+ }
37
+ export function createAdvancedHandler(opts) {
38
+ const upstream = opts.upstream.replace(/\/$/, '');
39
+ const prefix = opts.prefix ?? 'http';
40
+ const fetchImpl = opts.fetchImpl ?? ((u, i) => fetch(u, i));
41
+ const now = opts.now ?? Date.now;
42
+ const calls = new Map();
43
+ const seenKeys = new Map();
44
+ const agent = opts.agent;
45
+ const refuse = async (target, base, status, reason) => {
46
+ await agent.record({ target, payload: { ...base, refused: reason } });
47
+ return new Response(JSON.stringify({ error: reason }), { status, headers: { 'content-type': 'application/json' } });
48
+ };
49
+ return async (request) => {
50
+ const url = new URL(request.url);
51
+ const method = request.method.toUpperCase();
52
+ const target = targetFor(prefix, method, url.pathname);
53
+ const base = { method, path: url.pathname };
54
+ const body = method === 'GET' || method === 'HEAD' ? undefined : await request.text();
55
+ if (opts.allowPaths && !opts.allowPaths.some((p) => url.pathname === p || url.pathname.startsWith(p.endsWith('/') ? p : p + '/'))) {
56
+ return refuse(target, base, 403, `path ${url.pathname} is not on the egress allow-list`);
57
+ }
58
+ for (const [p, l] of Object.entries(opts.rateLimits ?? {})) {
59
+ if (!url.pathname.startsWith(p))
60
+ continue;
61
+ const t = now();
62
+ const recent = (calls.get(p) ?? []).filter((x) => t - x < l.windowMs);
63
+ if (recent.length >= l.max)
64
+ return refuse(target, base, 429, `rate limit for ${p}: ${l.max} per ${l.windowMs} ms`);
65
+ recent.push(t);
66
+ calls.set(p, recent);
67
+ }
68
+ const key = request.headers.get('idempotency-key');
69
+ if (opts.replayWindowMs !== undefined && key) {
70
+ const prev = seenKeys.get(key);
71
+ if (prev !== undefined && now() - prev < opts.replayWindowMs)
72
+ return refuse(target, { ...base, idempotency_key: sha(key) }, 409, 'repeated idempotency key: possible replay');
73
+ seenKeys.set(key, now());
74
+ }
75
+ if (body !== undefined && opts.bodyCommitment?.request) {
76
+ const nonce = randomBytes(16).toString('hex');
77
+ base.request_body = sha(nonce + body);
78
+ opts.bodyCommitment.onNonce?.(target, 'request', nonce);
79
+ }
80
+ if (body !== undefined && opts.detectPii) {
81
+ const types = detectPiiTypes(body);
82
+ if (types.length)
83
+ base.pii = types;
84
+ }
85
+ const headers = {};
86
+ request.headers.forEach((v, k) => { if (k.toLowerCase() !== 'host')
87
+ headers[k] = v; });
88
+ try {
89
+ const res = await fetchImpl(`${upstream}${url.pathname}${url.search}`, { method, headers, body });
90
+ const buf = new Uint8Array(await res.arrayBuffer());
91
+ const payload = { ...base, status: res.status };
92
+ if (opts.responseHash)
93
+ payload.response = sha(buf);
94
+ if (opts.bodyCommitment?.response) {
95
+ const nonce = randomBytes(16).toString('hex');
96
+ payload.response_body = sha(Buffer.concat([Buffer.from(nonce), Buffer.from(buf)]));
97
+ opts.bodyCommitment.onNonce?.(target, 'response', nonce);
98
+ }
99
+ const c = opts.cost?.({ method, path: url.pathname, status: res.status });
100
+ if (c)
101
+ Object.assign(payload, c);
102
+ await agent.record({ target, payload });
103
+ return new Response(buf, { status: res.status, headers: res.headers });
104
+ }
105
+ catch (err) {
106
+ await agent.record({ target, payload: { ...base, error: String(err) } });
107
+ throw err;
108
+ }
109
+ };
110
+ }
111
+ //# sourceMappingURL=advanced.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"advanced.js","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -27,4 +27,5 @@ export declare function createZaniiHandler(opts: GatewayOptions): (request: Requ
27
27
  * Node `http` server request listener wrapping the handler — for `http.createServer`.
28
28
  */
29
29
  export declare function createZaniiGateway(opts: GatewayOptions): (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => Promise<void>;
30
+ export * from './advanced.js';
30
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEjF,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAOD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuBhG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,IAEvC,KAAK,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,OAAO,WAAW,EAAE,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CAkBhH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEjF,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAOD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuBhG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,IAEvC,KAAK,OAAO,WAAW,EAAE,eAAe,EAAE,KAAK,OAAO,WAAW,EAAE,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CAkBhH;AACD,cAAc,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -57,4 +57,5 @@ export function createZaniiGateway(opts) {
57
57
  }
58
58
  };
59
59
  }
60
+ export * from './advanced.js';
60
61
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanii/gateway",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Drop-in proxy that receipts every HTTP API call an agent makes - zero code changes. Edge-compatible (fetch handler) plus a Node server and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ledger.zanii.agency",
@@ -32,11 +32,11 @@
32
32
  "@types/node": "^26.1.0",
33
33
  "typescript": "^5.8.2",
34
34
  "vitest": "^3.0.9",
35
- "@zanii/sdk": "0.5.2",
35
+ "@zanii/sdk": "0.5.3",
36
36
  "@zanii/testing": "0.1.2"
37
37
  },
38
38
  "dependencies": {
39
- "@zanii/sdk": "^0.5.2"
39
+ "@zanii/sdk": "^0.5.3"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.build.json",
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A gateway with more controls, all opt-in: salted body commitments, an egress allow-list,
3
+ * PII detection, per-call cost, a response-integrity hash, rate limits per route, and
4
+ * replay protection on idempotency keys. Refused calls never reach the upstream and are
5
+ * still receipted. `createZaniiHandler` itself is unchanged.
6
+ */
7
+ import { createHash, randomBytes } from 'node:crypto';
8
+ import type { ZaniiAgent } from '@zanii/sdk';
9
+ import type { FetchLike, GatewayOptions } from './index.js';
10
+
11
+ export interface AdvancedOptions extends GatewayOptions {
12
+ /** Commit request and/or response bodies with a fresh salt. `onNonce` receives each salt to keep. */
13
+ bodyCommitment?: { request?: boolean; response?: boolean; onNonce?: (target: string, kind: 'request' | 'response', nonce: string) => void };
14
+ /** Only these path prefixes may be forwarded. */
15
+ allowPaths?: string[];
16
+ /** Flag personal data in request bodies (types only are recorded, never the values). */
17
+ detectPii?: boolean;
18
+ /** Price a call (integer micro-units). */
19
+ cost?: (call: { method: string; path: string; status: number }) => { cost_micro: number; currency: string } | undefined;
20
+ /** Hash the response body into the receipt (proves what the upstream returned). */
21
+ responseHash?: boolean;
22
+ /** Calls per window per path prefix. */
23
+ rateLimits?: Record<string, { max: number; windowMs: number }>;
24
+ /** Refuse a repeated `idempotency-key` within the window (stops double payments). */
25
+ replayWindowMs?: number;
26
+ now?: () => number;
27
+ }
28
+
29
+ type Agent = Pick<ZaniiAgent, 'record'>;
30
+
31
+ const PII: Array<[string, RegExp]> = [
32
+ ['email', /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i],
33
+ ['phone', /(?:\+|00)\d[\d\s-]{7,14}\d/],
34
+ ['emirates-id', /\b784-?\d{4}-?\d{7}-?\d\b/],
35
+ ['iban', /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/],
36
+ ];
37
+
38
+ function luhn(digits: string): boolean {
39
+ let sum = 0;
40
+ for (let i = 0; i < digits.length; i++) {
41
+ let d = Number(digits[digits.length - 1 - i]);
42
+ if (i % 2) d = d * 2 > 9 ? d * 2 - 9 : d * 2;
43
+ sum += d;
44
+ }
45
+ return sum % 10 === 0;
46
+ }
47
+
48
+ /** Which kinds of personal data appear in a text (the values are never returned). */
49
+ export function detectPiiTypes(text: string): string[] {
50
+ const found = PII.filter(([, re]) => re.test(text)).map(([t]) => t);
51
+ const cards = text.match(/\b(?:\d[ -]?){13,19}\b/g) ?? [];
52
+ if (cards.some((c) => luhn(c.replace(/\D/g, '')))) found.push('payment-card');
53
+ return found;
54
+ }
55
+
56
+ const sha = (b: string | Uint8Array) => 'sha256:' + createHash('sha256').update(b).digest('hex');
57
+
58
+ function targetFor(prefix: string, method: string, pathname: string): string {
59
+ const path = pathname.replace(/[^a-z0-9]+/gi, '.').replace(/^\.|\.$/g, '') || 'root';
60
+ return `${prefix}.${method.toLowerCase()}.${path}`;
61
+ }
62
+
63
+ export function createAdvancedHandler(opts: AdvancedOptions): (request: Request) => Promise<Response> {
64
+ const upstream = opts.upstream.replace(/\/$/, '');
65
+ const prefix = opts.prefix ?? 'http';
66
+ const fetchImpl: FetchLike = opts.fetchImpl ?? ((u, i) => fetch(u, i));
67
+ const now = opts.now ?? Date.now;
68
+ const calls = new Map<string, number[]>();
69
+ const seenKeys = new Map<string, number>();
70
+ const agent: Agent = opts.agent;
71
+
72
+ const refuse = async (target: string, base: Record<string, unknown>, status: number, reason: string) => {
73
+ await agent.record({ target, payload: { ...base, refused: reason } });
74
+ return new Response(JSON.stringify({ error: reason }), { status, headers: { 'content-type': 'application/json' } });
75
+ };
76
+
77
+ return async (request) => {
78
+ const url = new URL(request.url);
79
+ const method = request.method.toUpperCase();
80
+ const target = targetFor(prefix, method, url.pathname);
81
+ const base: Record<string, unknown> = { method, path: url.pathname };
82
+ const body = method === 'GET' || method === 'HEAD' ? undefined : await request.text();
83
+
84
+ if (opts.allowPaths && !opts.allowPaths.some((p) => url.pathname === p || url.pathname.startsWith(p.endsWith('/') ? p : p + '/'))) {
85
+ return refuse(target, base, 403, `path ${url.pathname} is not on the egress allow-list`);
86
+ }
87
+ for (const [p, l] of Object.entries(opts.rateLimits ?? {})) {
88
+ if (!url.pathname.startsWith(p)) continue;
89
+ const t = now();
90
+ const recent = (calls.get(p) ?? []).filter((x) => t - x < l.windowMs);
91
+ if (recent.length >= l.max) return refuse(target, base, 429, `rate limit for ${p}: ${l.max} per ${l.windowMs} ms`);
92
+ recent.push(t);
93
+ calls.set(p, recent);
94
+ }
95
+ const key = request.headers.get('idempotency-key');
96
+ if (opts.replayWindowMs !== undefined && key) {
97
+ const prev = seenKeys.get(key);
98
+ if (prev !== undefined && now() - prev < opts.replayWindowMs) return refuse(target, { ...base, idempotency_key: sha(key) }, 409, 'repeated idempotency key: possible replay');
99
+ seenKeys.set(key, now());
100
+ }
101
+ if (body !== undefined && opts.bodyCommitment?.request) {
102
+ const nonce = randomBytes(16).toString('hex');
103
+ base.request_body = sha(nonce + body);
104
+ opts.bodyCommitment.onNonce?.(target, 'request', nonce);
105
+ }
106
+ if (body !== undefined && opts.detectPii) {
107
+ const types = detectPiiTypes(body);
108
+ if (types.length) base.pii = types;
109
+ }
110
+
111
+ const headers: Record<string, string> = {};
112
+ request.headers.forEach((v, k) => { if (k.toLowerCase() !== 'host') headers[k] = v; });
113
+ try {
114
+ const res = await fetchImpl(`${upstream}${url.pathname}${url.search}`, { method, headers, body });
115
+ const buf = new Uint8Array(await res.arrayBuffer());
116
+ const payload: Record<string, unknown> = { ...base, status: res.status };
117
+ if (opts.responseHash) payload.response = sha(buf);
118
+ if (opts.bodyCommitment?.response) {
119
+ const nonce = randomBytes(16).toString('hex');
120
+ payload.response_body = sha(Buffer.concat([Buffer.from(nonce), Buffer.from(buf)]));
121
+ opts.bodyCommitment.onNonce?.(target, 'response', nonce);
122
+ }
123
+ const c = opts.cost?.({ method, path: url.pathname, status: res.status });
124
+ if (c) Object.assign(payload, c);
125
+ await agent.record({ target, payload });
126
+ return new Response(buf, { status: res.status, headers: res.headers });
127
+ } catch (err) {
128
+ await agent.record({ target, payload: { ...base, error: String(err) } });
129
+ throw err;
130
+ }
131
+ };
132
+ }
package/src/index.ts CHANGED
@@ -79,3 +79,4 @@ export function createZaniiGateway(opts: GatewayOptions) {
79
79
  }
80
80
  };
81
81
  }
82
+ export * from './advanced.js';