@aiti-ai/ddns-mocks 0.0.0-stage → 0.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.
@@ -0,0 +1,26 @@
1
+ /** Generic dyndns2 mock — serves any of the update-only services: checks
2
+ * basic auth (when configured), records hostname/myip, and returns a
3
+ * configurable body so success/nochg/error semantics can be tested. */
4
+ export interface DyndnsMockOptions {
5
+ /** Require this basic-auth credential ("user:pass"). Empty = skip auth. */
6
+ auth?: string;
7
+ /** Response body (default "good 1.2.3.4"). */
8
+ okText?: string;
9
+ /** Response body when no basic auth matches (default "badauth"). */
10
+ badAuthText?: string;
11
+ port?: number;
12
+ }
13
+ export interface DyndnsMockState {
14
+ requests: Array<{
15
+ url: string;
16
+ hostname: string | null;
17
+ myip: string | null;
18
+ auth: string | null;
19
+ }>;
20
+ }
21
+ export interface DyndnsMockHandle {
22
+ url: string;
23
+ state: DyndnsMockState;
24
+ stop(): Promise<void>;
25
+ }
26
+ export declare function startDyndnsMock(opts?: DyndnsMockOptions): Promise<DyndnsMockHandle>;
package/dist/dyndns.js ADDED
@@ -0,0 +1,30 @@
1
+ /** Generic dyndns2 mock — serves any of the update-only services: checks
2
+ * basic auth (when configured), records hostname/myip, and returns a
3
+ * configurable body so success/nochg/error semantics can be tested. */
4
+ import { createServer } from "node:http";
5
+ export async function startDyndnsMock(opts = {}) {
6
+ const state = { requests: [] };
7
+ const server = createServer((req, res) => {
8
+ const url = new URL(req.url ?? "/", "http://localhost");
9
+ const auth = req.headers.authorization ?? null;
10
+ const expected = opts.auth ? `Basic ${Buffer.from(opts.auth).toString("base64")}` : null;
11
+ state.requests.push({
12
+ url: url.pathname + url.search,
13
+ hostname: url.searchParams.get("hostname"),
14
+ myip: url.searchParams.get("myip"),
15
+ auth,
16
+ });
17
+ res.writeHead(200, { "content-type": "text/plain" });
18
+ if (expected && auth !== expected)
19
+ return res.end(opts.badAuthText ?? "badauth");
20
+ res.end(opts.okText ?? "good 203.0.113.1");
21
+ });
22
+ await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
23
+ const addr = server.address();
24
+ const port = typeof addr === "object" && addr ? addr.port : 0;
25
+ return {
26
+ url: `http://127.0.0.1:${port}`,
27
+ state,
28
+ stop: () => new Promise((resolve) => server.close(() => resolve())),
29
+ };
30
+ }
@@ -0,0 +1,23 @@
1
+ /** freedns mock — in-memory sync.afraid.org (Go ddns-updater shape:
2
+ * GET /u/<token>/, response text decides success). Also accepts the
3
+ * optional ?value= param (the classic afraid.org update API) for clients
4
+ * that push arbitrary values. */
5
+ export interface FreednsMockOptions {
6
+ token?: string;
7
+ /** Response body on success (default "updated 203.0.113.1"). */
8
+ okText?: string;
9
+ port?: number;
10
+ }
11
+ export interface FreednsMockState {
12
+ requests: Array<{
13
+ url: string;
14
+ token: string | null;
15
+ value: string | null;
16
+ }>;
17
+ }
18
+ export interface FreednsMockHandle {
19
+ url: string;
20
+ state: FreednsMockState;
21
+ stop(): Promise<void>;
22
+ }
23
+ export declare function startFreednsMock(opts?: FreednsMockOptions): Promise<FreednsMockHandle>;
@@ -0,0 +1,28 @@
1
+ /** freedns mock — in-memory sync.afraid.org (Go ddns-updater shape:
2
+ * GET /u/<token>/, response text decides success). Also accepts the
3
+ * optional ?value= param (the classic afraid.org update API) for clients
4
+ * that push arbitrary values. */
5
+ import { createServer } from "node:http";
6
+ export async function startFreednsMock(opts = {}) {
7
+ const token = opts.token ?? "freedns-token";
8
+ const state = { requests: [] };
9
+ const server = createServer((req, res) => {
10
+ const url = new URL(req.url ?? "/", "http://localhost");
11
+ const m = /^\/u\/([^/]+)\/?$/.exec(url.pathname);
12
+ const tok = m?.[1] ?? null;
13
+ const value = url.searchParams.get("value");
14
+ state.requests.push({ url: url.pathname + url.search, token: tok, value });
15
+ res.writeHead(200, { "content-type": "text/plain" });
16
+ if (tok !== token)
17
+ return res.end("invalid token");
18
+ res.end(opts.okText ?? "updated 203.0.113.1");
19
+ });
20
+ await new Promise((resolve) => server.listen(opts.port ?? 0, "127.0.0.1", resolve));
21
+ const addr = server.address();
22
+ const port = typeof addr === "object" && addr ? addr.port : 0;
23
+ return {
24
+ url: `http://127.0.0.1:${port}`,
25
+ state,
26
+ stop: () => new Promise((resolve) => server.close(() => resolve())),
27
+ };
28
+ }
@@ -0,0 +1,74 @@
1
+ /** mocks — one in-memory mock per provider, mirroring the REAL provider
2
+ * HTTP APIs closely enough that third-party implementations (Go
3
+ * ddns-updater, Python lexicon) pointed at the mock also work:
4
+ *
5
+ * cloudflare → api.cloudflare.com/client/v4 (REST, Bearer token)
6
+ * dnspod → dnsapi.cn Record.* (form POST, login_token)
7
+ * aliyun → alidns.aliyuncs.com (signed RPC; signature verified when
8
+ * the mock is given the AccessKeySecret)
9
+ * duckdns → www.duckdns.org/v3/update ("OK"/"KO")
10
+ * custom → arbitrary webhook URL (success regex semantics)
11
+ *
12
+ * Every mock returns a handle with `url` to inject as the provider's
13
+ * baseUrl and a `state` object to assert on. Start one with
14
+ * startMockProvider(name, opts), or all of them with startAllMocks().
15
+ */
16
+ export type MockProviderName = string;
17
+ import { type CloudflareMockHandle, type CloudflareMockOptions } from "./cloudflare.ts";
18
+ import { type DnspodMockHandle, type DnspodMockOptions } from "./dnspod.ts";
19
+ import { type AliyunMockHandle, type AliyunMockOptions } from "./aliyun.ts";
20
+ import { type DuckdnsMockHandle, type DuckdnsMockOptions } from "./duckdns.ts";
21
+ import { type CustomMockHandle, type CustomMockOptions } from "./custom.ts";
22
+ import { type FreednsMockHandle, type FreednsMockOptions } from "./freedns.ts";
23
+ import { type RestMockHandle } from "./rest.ts";
24
+ import { type DyndnsMockHandle } from "./dyndns.ts";
25
+ import { type LooseMockHandle } from "./loose.ts";
26
+ import { type Route53MockHandle } from "./route53.ts";
27
+ export type AnyMockHandle = CloudflareMockHandle | DnspodMockHandle | AliyunMockHandle | DuckdnsMockHandle | CustomMockHandle | FreednsMockHandle | RestMockHandle | DyndnsMockHandle | LooseMockHandle | Route53MockHandle;
28
+ export type MockOptionsMap = {
29
+ cloudflare: CloudflareMockOptions;
30
+ dnspod: DnspodMockOptions;
31
+ aliyun: AliyunMockOptions;
32
+ duckdns: DuckdnsMockOptions;
33
+ custom: CustomMockOptions;
34
+ freedns: FreednsMockOptions;
35
+ };
36
+ /** Start a mock for ANY registered provider name (rest specs + dyndns
37
+ * services get their generic mocks automatically). */
38
+ export declare function startMockFor(name: string, opts?: {
39
+ zone?: string;
40
+ cred?: string;
41
+ records?: Array<{
42
+ type: string;
43
+ name: string;
44
+ value: string;
45
+ ttl?: number;
46
+ }>;
47
+ port?: number;
48
+ }): Promise<AnyMockHandle>;
49
+ export declare function startMockProvider(name: MockProviderName, opts?: Record<string, unknown>): Promise<AnyMockHandle>;
50
+ export interface AllMocksHandle {
51
+ handles: Record<MockProviderName, AnyMockHandle>;
52
+ stopAll(): Promise<void>;
53
+ }
54
+ /** Start every provider mock (aliyun needs a zone). */
55
+ export declare function startAllMocks(opts?: {
56
+ aliyunZone: string;
57
+ }): Promise<AllMocksHandle>;
58
+ export { startCloudflareMock } from "./cloudflare.ts";
59
+ export { startDnspodMock } from "./dnspod.ts";
60
+ export { startAliyunMock } from "./aliyun.ts";
61
+ export { startDuckdnsMock } from "./duckdns.ts";
62
+ export { startCustomMock } from "./custom.ts";
63
+ export { startFreednsMock } from "./freedns.ts";
64
+ export { startRestMock } from "./rest.ts";
65
+ export { startDyndnsMock } from "./dyndns.ts";
66
+ export { startTlsProxy, caCertPath, serverCertPath, MOCK_HOSTS } from "./tls.ts";
67
+ export type { CloudflareMockHandle, CloudflareMockOptions, CloudflareMockState } from "./cloudflare.ts";
68
+ export type { DnspodMockHandle, DnspodMockOptions, DnspodMockState } from "./dnspod.ts";
69
+ export type { AliyunMockHandle, AliyunMockOptions, AliyunMockState } from "./aliyun.ts";
70
+ export type { DuckdnsMockHandle, DuckdnsMockOptions, DuckdnsMockState } from "./duckdns.ts";
71
+ export type { CustomMockHandle, CustomMockOptions, CustomMockState } from "./custom.ts";
72
+ export type { FreednsMockHandle, FreednsMockOptions, FreednsMockState } from "./freedns.ts";
73
+ export type { RestMockHandle, RestMockOptions, RestMockState } from "./rest.ts";
74
+ export type { DyndnsMockHandle, DyndnsMockOptions, DyndnsMockState } from "./dyndns.ts";
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ /** mocks — one in-memory mock per provider, mirroring the REAL provider
2
+ * HTTP APIs closely enough that third-party implementations (Go
3
+ * ddns-updater, Python lexicon) pointed at the mock also work:
4
+ *
5
+ * cloudflare → api.cloudflare.com/client/v4 (REST, Bearer token)
6
+ * dnspod → dnsapi.cn Record.* (form POST, login_token)
7
+ * aliyun → alidns.aliyuncs.com (signed RPC; signature verified when
8
+ * the mock is given the AccessKeySecret)
9
+ * duckdns → www.duckdns.org/v3/update ("OK"/"KO")
10
+ * custom → arbitrary webhook URL (success regex semantics)
11
+ *
12
+ * Every mock returns a handle with `url` to inject as the provider's
13
+ * baseUrl and a `state` object to assert on. Start one with
14
+ * startMockProvider(name, opts), or all of them with startAllMocks().
15
+ */
16
+ import { startCloudflareMock } from "./cloudflare.js";
17
+ import { startDnspodMock } from "./dnspod.js";
18
+ import { startAliyunMock } from "./aliyun.js";
19
+ import { startDuckdnsMock } from "./duckdns.js";
20
+ import { startCustomMock } from "./custom.js";
21
+ import { startFreednsMock } from "./freedns.js";
22
+ import { startRestMock } from "./rest.js";
23
+ import { startDyndnsMock } from "./dyndns.js";
24
+ import { startLooseZoneMock } from "./loose.js";
25
+ import { startRoute53Mock } from "./route53.js";
26
+ import { registry } from "@aiti-ai/ddns/plugins";
27
+ /** Start a mock for ANY registered provider name (rest specs + dyndns
28
+ * services get their generic mocks automatically). */
29
+ export async function startMockFor(name, opts = {}) {
30
+ const { REST_SPECS } = await import("@aiti-ai/ddns/rest-specs");
31
+ const { DYNDNS_SERVICES } = await import("@aiti-ai/ddns/dyndns-services");
32
+ const rest = REST_SPECS.find((s) => s.name === name);
33
+ if (rest)
34
+ return startRestMock({ spec: rest, zone: opts.zone ?? "example.com", cred: opts.cred, records: opts.records, port: opts.port });
35
+ const dyn = DYNDNS_SERVICES.find((s) => s.name === name);
36
+ if (dyn)
37
+ return startDyndnsMock({ auth: dyn.tokenOnly ? undefined : "user:pass", okText: `${dyn.successPrefixes[0] ?? "good"} 1.2.3.4`, port: opts.port });
38
+ // hand-written providers without a dedicated mock: loose zone REST mock
39
+ return startLooseZoneMock({ provider: name, auth: "none", cred: "test-token", ...(opts.zone ? { zone: opts.zone } : {}), ...(opts.records ? { records: opts.records } : {}), ...(opts.port ? { port: opts.port } : {}) });
40
+ }
41
+ export async function startMockProvider(name, opts = {}) {
42
+ switch (name) {
43
+ case "cloudflare": return startCloudflareMock(opts);
44
+ case "dnspod": return startDnspodMock(opts);
45
+ case "aliyun": return startAliyunMock(opts);
46
+ case "duckdns": return startDuckdnsMock(opts);
47
+ case "custom": return startCustomMock(opts);
48
+ case "freedns": return startFreednsMock(opts);
49
+ case "route53": return startRoute53Mock(opts);
50
+ default: {
51
+ const pluginMock = registry.getMock(name);
52
+ if (pluginMock)
53
+ return pluginMock(opts);
54
+ return startMockFor(name, opts);
55
+ }
56
+ }
57
+ }
58
+ /** Start every provider mock (aliyun needs a zone). */
59
+ export async function startAllMocks(opts = { aliyunZone: "example.com" }) {
60
+ const [cloudflare, dnspod, aliyun, duckdns, custom, freedns] = await Promise.all([
61
+ startCloudflareMock(),
62
+ startDnspodMock(),
63
+ startAliyunMock({ zone: opts.aliyunZone }),
64
+ startDuckdnsMock(),
65
+ startCustomMock(),
66
+ startFreednsMock(),
67
+ ]);
68
+ return {
69
+ handles: { cloudflare, dnspod, aliyun, duckdns, custom, freedns },
70
+ stopAll: async () => {
71
+ await Promise.all([cloudflare.stop(), dnspod.stop(), aliyun.stop(), duckdns.stop(), custom.stop(), freedns.stop()]);
72
+ },
73
+ };
74
+ }
75
+ export { startCloudflareMock } from "./cloudflare.js";
76
+ export { startDnspodMock } from "./dnspod.js";
77
+ export { startAliyunMock } from "./aliyun.js";
78
+ export { startDuckdnsMock } from "./duckdns.js";
79
+ export { startCustomMock } from "./custom.js";
80
+ export { startFreednsMock } from "./freedns.js";
81
+ export { startRestMock } from "./rest.js";
82
+ export { startDyndnsMock } from "./dyndns.js";
83
+ export { startTlsProxy, caCertPath, serverCertPath, MOCK_HOSTS } from "./tls.js";
@@ -0,0 +1,58 @@
1
+ /** Loose zone REST mock — one shared mock for the hand-written providers
2
+ * whose CRUD follows the common zone-REST pattern (GET list, POST create,
3
+ * PUT update, DELETE by id) with varying paths/auth. It answers ANY path:
4
+ *
5
+ * GET → returns stored records (superset apiShape — every fields
6
+ * mapping in rest-specs can read name/content/data/…)
7
+ * POST → creates a record (parses name/value/type from any common
8
+ * body field via stdRec: name/host/domain, data/content/value)
9
+ * PUT → updates by trailing id segment
10
+ * DELETE → removes by trailing id segment
11
+ *
12
+ * Auth is checked per kind: bearer / basic / none / token-in-body.
13
+ * This is what makes every hand-written provider verifiable through
14
+ * verify-all.mjs (ours + lexicon over HTTPS_PROXY).
15
+ */
16
+ export interface LooseMockOptions {
17
+ /** Provider name for response-shape adaptation. */
18
+ provider?: string;
19
+ /** Authentication the mock enforces. Default "bearer". */
20
+ auth?: "bearer" | "basic" | "none";
21
+ /** Credential to accept. Default "test-token"; basic = "user:pass". */
22
+ cred?: string;
23
+ /** Pre-seeded records ({type,name,value,ttl}). */
24
+ records?: Array<{
25
+ type: string;
26
+ name: string;
27
+ value: string;
28
+ ttl?: number;
29
+ }>;
30
+ /** Extra auth key the provider must include (e.g. custom header). */
31
+ header?: {
32
+ key: string;
33
+ value: string;
34
+ };
35
+ port?: number;
36
+ }
37
+ export interface LooseMockState {
38
+ records: Array<{
39
+ id: string;
40
+ type: string;
41
+ name: string;
42
+ value: string;
43
+ ttl?: number;
44
+ }>;
45
+ requests: Array<{
46
+ method: string;
47
+ url: string;
48
+ body?: unknown;
49
+ }>;
50
+ }
51
+ export interface LooseMockHandle {
52
+ url: string;
53
+ state: LooseMockState;
54
+ stop(): Promise<void>;
55
+ }
56
+ declare const defaultFetchLike: null;
57
+ export declare function startLooseZoneMock(opts?: LooseMockOptions): Promise<LooseMockHandle>;
58
+ export { defaultFetchLike };