@vivoa/plugin-sdk 0.6.5 → 0.6.6
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/dist/chunk-DGJZW46B.mjs +37 -0
- package/dist/host-api-C1VylW5l.d.mts +1416 -0
- package/dist/host-api-C1VylW5l.d.ts +1416 -0
- package/dist/index.d.mts +5 -1382
- package/dist/index.d.ts +5 -1382
- package/dist/index.js +29 -1
- package/dist/index.mjs +33 -33
- package/dist/testing/index.d.mts +54 -0
- package/dist/testing/index.d.ts +54 -0
- package/dist/testing/index.js +169 -0
- package/dist/testing/index.mjs +113 -0
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ANALYTICS_PLUGIN_TOKEN: () => ANALYTICS_PLUGIN_TOKEN,
|
|
24
|
+
AUTHOR_HANDLE_RE: () => AUTHOR_HANDLE_RE,
|
|
24
25
|
ActionVariant: () => ActionVariant,
|
|
25
26
|
AnalyticsFeature: () => AnalyticsFeature,
|
|
26
27
|
BlockType: () => BlockType,
|
|
@@ -62,6 +63,7 @@ __export(index_exports, {
|
|
|
62
63
|
paymentCapability: () => paymentCapability,
|
|
63
64
|
shippingCapability: () => shippingCapability,
|
|
64
65
|
suppliersCapability: () => suppliersCapability,
|
|
66
|
+
validatePluginAuthor: () => validatePluginAuthor,
|
|
65
67
|
validateSetupLayout: () => validateSetupLayout
|
|
66
68
|
});
|
|
67
69
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -244,6 +246,30 @@ var InvoiceStatus = /* @__PURE__ */ ((InvoiceStatus2) => {
|
|
|
244
246
|
return InvoiceStatus2;
|
|
245
247
|
})(InvoiceStatus || {});
|
|
246
248
|
|
|
249
|
+
// src/author.ts
|
|
250
|
+
var AUTHOR_HANDLE_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
251
|
+
function validatePluginAuthor(value) {
|
|
252
|
+
const errors = [];
|
|
253
|
+
if (typeof value !== "object" || value === null) {
|
|
254
|
+
return { ok: false, errors: ["author is required and must be an object"] };
|
|
255
|
+
}
|
|
256
|
+
const a = value;
|
|
257
|
+
if (typeof a.handle !== "string" || a.handle.length === 0) {
|
|
258
|
+
errors.push("author.handle is required");
|
|
259
|
+
} else if (!AUTHOR_HANDLE_RE.test(a.handle)) {
|
|
260
|
+
errors.push(
|
|
261
|
+
`author.handle "${a.handle}" must be a lowercase slug (a-z, 0-9, dash; start alphanumeric)`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (typeof a.displayName !== "string" || a.displayName.trim().length === 0) {
|
|
265
|
+
errors.push("author.displayName is required");
|
|
266
|
+
}
|
|
267
|
+
if (a.url !== void 0 && typeof a.url !== "string") {
|
|
268
|
+
errors.push("author.url must be a string when present");
|
|
269
|
+
}
|
|
270
|
+
return { ok: errors.length === 0, errors };
|
|
271
|
+
}
|
|
272
|
+
|
|
247
273
|
// src/setup-layout.ts
|
|
248
274
|
var import_superstruct2 = require("superstruct");
|
|
249
275
|
var FieldVariant = /* @__PURE__ */ ((FieldVariant2) => {
|
|
@@ -581,7 +607,7 @@ function checkPermissions(required, grantedRaw) {
|
|
|
581
607
|
}
|
|
582
608
|
|
|
583
609
|
// src/version.ts
|
|
584
|
-
var SDK_VERSION = "0.6.
|
|
610
|
+
var SDK_VERSION = "0.6.6";
|
|
585
611
|
function isSdkCompatible(pluginSdkVersion, hostSdkVersion) {
|
|
586
612
|
const parse = (v) => {
|
|
587
613
|
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
@@ -599,6 +625,7 @@ function isSdkCompatible(pluginSdkVersion, hostSdkVersion) {
|
|
|
599
625
|
// Annotate the CommonJS export names for ESM import in node:
|
|
600
626
|
0 && (module.exports = {
|
|
601
627
|
ANALYTICS_PLUGIN_TOKEN,
|
|
628
|
+
AUTHOR_HANDLE_RE,
|
|
602
629
|
ActionVariant,
|
|
603
630
|
AnalyticsFeature,
|
|
604
631
|
BlockType,
|
|
@@ -640,5 +667,6 @@ function isSdkCompatible(pluginSdkVersion, hostSdkVersion) {
|
|
|
640
667
|
paymentCapability,
|
|
641
668
|
shippingCapability,
|
|
642
669
|
suppliersCapability,
|
|
670
|
+
validatePluginAuthor,
|
|
643
671
|
validateSetupLayout
|
|
644
672
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkPermissions,
|
|
3
|
+
isPermissionGranted,
|
|
4
|
+
parsePermission
|
|
5
|
+
} from "./chunk-DGJZW46B.mjs";
|
|
6
|
+
|
|
1
7
|
// src/tokens.ts
|
|
2
8
|
var PAYMENT_PLUGIN_TOKEN = "PAYMENT_PLUGINS";
|
|
3
9
|
var SHIPPING_PLUGIN_TOKEN = "SHIPPING_PLUGINS";
|
|
@@ -184,6 +190,30 @@ var InvoiceStatus = /* @__PURE__ */ ((InvoiceStatus2) => {
|
|
|
184
190
|
return InvoiceStatus2;
|
|
185
191
|
})(InvoiceStatus || {});
|
|
186
192
|
|
|
193
|
+
// src/author.ts
|
|
194
|
+
var AUTHOR_HANDLE_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
195
|
+
function validatePluginAuthor(value) {
|
|
196
|
+
const errors = [];
|
|
197
|
+
if (typeof value !== "object" || value === null) {
|
|
198
|
+
return { ok: false, errors: ["author is required and must be an object"] };
|
|
199
|
+
}
|
|
200
|
+
const a = value;
|
|
201
|
+
if (typeof a.handle !== "string" || a.handle.length === 0) {
|
|
202
|
+
errors.push("author.handle is required");
|
|
203
|
+
} else if (!AUTHOR_HANDLE_RE.test(a.handle)) {
|
|
204
|
+
errors.push(
|
|
205
|
+
`author.handle "${a.handle}" must be a lowercase slug (a-z, 0-9, dash; start alphanumeric)`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (typeof a.displayName !== "string" || a.displayName.trim().length === 0) {
|
|
209
|
+
errors.push("author.displayName is required");
|
|
210
|
+
}
|
|
211
|
+
if (a.url !== void 0 && typeof a.url !== "string") {
|
|
212
|
+
errors.push("author.url must be a string when present");
|
|
213
|
+
}
|
|
214
|
+
return { ok: errors.length === 0, errors };
|
|
215
|
+
}
|
|
216
|
+
|
|
187
217
|
// src/setup-layout.ts
|
|
188
218
|
import {
|
|
189
219
|
array,
|
|
@@ -501,40 +531,8 @@ function isPluginFactory(value) {
|
|
|
501
531
|
return typeof value === "function" && value[PLUGIN_FACTORY_BRAND] === true;
|
|
502
532
|
}
|
|
503
533
|
|
|
504
|
-
// src/permissions.ts
|
|
505
|
-
var PERMISSION_RE = /^([a-z0-9]+)\.([a-z0-9]+)(?::(.+))?$/i;
|
|
506
|
-
function parsePermission(raw) {
|
|
507
|
-
const m = PERMISSION_RE.exec(raw);
|
|
508
|
-
if (!m) return null;
|
|
509
|
-
const [, domain, action, resource] = m;
|
|
510
|
-
return resource === void 0 ? { domain, action } : { domain, action, resource };
|
|
511
|
-
}
|
|
512
|
-
function isPermissionGranted(required, granted) {
|
|
513
|
-
return granted.some((g) => {
|
|
514
|
-
if (g.domain !== required.domain || g.action !== required.action) {
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
if (required.resource === void 0) return true;
|
|
518
|
-
return g.resource === "*" || g.resource === required.resource;
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
function checkPermissions(required, grantedRaw) {
|
|
522
|
-
const grantedParsed = grantedRaw.map(parsePermission).filter((p) => p !== null);
|
|
523
|
-
const granted = [];
|
|
524
|
-
const missing = [];
|
|
525
|
-
for (const raw of required) {
|
|
526
|
-
const parsed = parsePermission(raw);
|
|
527
|
-
if (parsed && isPermissionGranted(parsed, grantedParsed)) {
|
|
528
|
-
granted.push(raw);
|
|
529
|
-
} else {
|
|
530
|
-
missing.push(raw);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return { ok: missing.length === 0, granted, missing };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
534
|
// src/version.ts
|
|
537
|
-
var SDK_VERSION = "0.6.
|
|
535
|
+
var SDK_VERSION = "0.6.6";
|
|
538
536
|
function isSdkCompatible(pluginSdkVersion, hostSdkVersion) {
|
|
539
537
|
const parse = (v) => {
|
|
540
538
|
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
@@ -551,6 +549,7 @@ function isSdkCompatible(pluginSdkVersion, hostSdkVersion) {
|
|
|
551
549
|
}
|
|
552
550
|
export {
|
|
553
551
|
ANALYTICS_PLUGIN_TOKEN,
|
|
552
|
+
AUTHOR_HANDLE_RE,
|
|
554
553
|
ActionVariant,
|
|
555
554
|
AnalyticsFeature,
|
|
556
555
|
BlockType,
|
|
@@ -592,5 +591,6 @@ export {
|
|
|
592
591
|
paymentCapability,
|
|
593
592
|
shippingCapability,
|
|
594
593
|
suppliersCapability,
|
|
594
|
+
validatePluginAuthor,
|
|
595
595
|
validateSetupLayout
|
|
596
596
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { X as HostApi, Z as HttpRequest, N as DomainEventName, u as Capability, M as DomainEventHandler, _ as HttpResponse, K as DispatchContext } from '../host-api-C1VylW5l.mjs';
|
|
2
|
+
|
|
3
|
+
/** Every recorded interaction, keyed by `"port.method"`. */
|
|
4
|
+
interface MockHostCalls {
|
|
5
|
+
'http.request': HttpRequest[];
|
|
6
|
+
'secrets.get': string[];
|
|
7
|
+
'storage.getUrl': string[];
|
|
8
|
+
'storage.put': Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
body: Uint8Array | string;
|
|
11
|
+
}>;
|
|
12
|
+
'sequence.next': string[];
|
|
13
|
+
'host.on': DomainEventName[];
|
|
14
|
+
}
|
|
15
|
+
interface MockHostOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Permissions the mock "grants". When set, the mock enforces them exactly as
|
|
18
|
+
* the real host is specified to: an un-granted `http.outbound:<host>` or
|
|
19
|
+
* `secrets.read:<key>` THROWS. Omit to grant everything (pure recorder mode).
|
|
20
|
+
*/
|
|
21
|
+
grantedPermissions?: string[];
|
|
22
|
+
/** Canned secret values returned by `secrets.get`. */
|
|
23
|
+
secrets?: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Canned HTTP responses. Resolved against the request URL — exact match
|
|
26
|
+
* first, then the first entry whose key is a substring of the URL. Defaults
|
|
27
|
+
* to `{ status: 200, headers: {}, body: {} }`.
|
|
28
|
+
*/
|
|
29
|
+
httpResponses?: Record<string, HttpResponse>;
|
|
30
|
+
/** Seed values for named sequence scopes (monotonic from here). */
|
|
31
|
+
sequences?: Record<string, number>;
|
|
32
|
+
}
|
|
33
|
+
interface MockHostApi extends HostApi {
|
|
34
|
+
/** Every port call recorded in order. */
|
|
35
|
+
readonly calls: MockHostCalls;
|
|
36
|
+
/** Capabilities the plugin registered via `registerCapability`. */
|
|
37
|
+
readonly registered: Capability[];
|
|
38
|
+
/** Event handlers the plugin subscribed via `host.on`. */
|
|
39
|
+
readonly handlers: Map<DomainEventName, DomainEventHandler[]>;
|
|
40
|
+
/** Drives a subscribed handler as if the host emitted the event. */
|
|
41
|
+
emit(event: DomainEventName, payload: unknown): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds a {@link MockHostApi}. Pass `grantedPermissions` to assert your plugin
|
|
45
|
+
* declared what it actually uses; leave it out to just record calls.
|
|
46
|
+
*/
|
|
47
|
+
declare function createMockHostApi(opts?: MockHostOptions): MockHostApi;
|
|
48
|
+
/**
|
|
49
|
+
* Minimal {@link DispatchContext} for dispatch-method tests. Pass the merged
|
|
50
|
+
* config your plugin expects; the country fields mirror the host's resolution.
|
|
51
|
+
*/
|
|
52
|
+
declare function createDispatchContext(config?: Record<string, unknown>, overrides?: Partial<DispatchContext>): DispatchContext;
|
|
53
|
+
|
|
54
|
+
export { type MockHostApi, type MockHostCalls, type MockHostOptions, createDispatchContext, createMockHostApi };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { X as HostApi, Z as HttpRequest, N as DomainEventName, u as Capability, M as DomainEventHandler, _ as HttpResponse, K as DispatchContext } from '../host-api-C1VylW5l.js';
|
|
2
|
+
|
|
3
|
+
/** Every recorded interaction, keyed by `"port.method"`. */
|
|
4
|
+
interface MockHostCalls {
|
|
5
|
+
'http.request': HttpRequest[];
|
|
6
|
+
'secrets.get': string[];
|
|
7
|
+
'storage.getUrl': string[];
|
|
8
|
+
'storage.put': Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
body: Uint8Array | string;
|
|
11
|
+
}>;
|
|
12
|
+
'sequence.next': string[];
|
|
13
|
+
'host.on': DomainEventName[];
|
|
14
|
+
}
|
|
15
|
+
interface MockHostOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Permissions the mock "grants". When set, the mock enforces them exactly as
|
|
18
|
+
* the real host is specified to: an un-granted `http.outbound:<host>` or
|
|
19
|
+
* `secrets.read:<key>` THROWS. Omit to grant everything (pure recorder mode).
|
|
20
|
+
*/
|
|
21
|
+
grantedPermissions?: string[];
|
|
22
|
+
/** Canned secret values returned by `secrets.get`. */
|
|
23
|
+
secrets?: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Canned HTTP responses. Resolved against the request URL — exact match
|
|
26
|
+
* first, then the first entry whose key is a substring of the URL. Defaults
|
|
27
|
+
* to `{ status: 200, headers: {}, body: {} }`.
|
|
28
|
+
*/
|
|
29
|
+
httpResponses?: Record<string, HttpResponse>;
|
|
30
|
+
/** Seed values for named sequence scopes (monotonic from here). */
|
|
31
|
+
sequences?: Record<string, number>;
|
|
32
|
+
}
|
|
33
|
+
interface MockHostApi extends HostApi {
|
|
34
|
+
/** Every port call recorded in order. */
|
|
35
|
+
readonly calls: MockHostCalls;
|
|
36
|
+
/** Capabilities the plugin registered via `registerCapability`. */
|
|
37
|
+
readonly registered: Capability[];
|
|
38
|
+
/** Event handlers the plugin subscribed via `host.on`. */
|
|
39
|
+
readonly handlers: Map<DomainEventName, DomainEventHandler[]>;
|
|
40
|
+
/** Drives a subscribed handler as if the host emitted the event. */
|
|
41
|
+
emit(event: DomainEventName, payload: unknown): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds a {@link MockHostApi}. Pass `grantedPermissions` to assert your plugin
|
|
45
|
+
* declared what it actually uses; leave it out to just record calls.
|
|
46
|
+
*/
|
|
47
|
+
declare function createMockHostApi(opts?: MockHostOptions): MockHostApi;
|
|
48
|
+
/**
|
|
49
|
+
* Minimal {@link DispatchContext} for dispatch-method tests. Pass the merged
|
|
50
|
+
* config your plugin expects; the country fields mirror the host's resolution.
|
|
51
|
+
*/
|
|
52
|
+
declare function createDispatchContext(config?: Record<string, unknown>, overrides?: Partial<DispatchContext>): DispatchContext;
|
|
53
|
+
|
|
54
|
+
export { type MockHostApi, type MockHostCalls, type MockHostOptions, createDispatchContext, createMockHostApi };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing/index.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
createDispatchContext: () => createDispatchContext,
|
|
24
|
+
createMockHostApi: () => createMockHostApi
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(testing_exports);
|
|
27
|
+
|
|
28
|
+
// src/permissions.ts
|
|
29
|
+
var PERMISSION_RE = /^([a-z0-9]+)\.([a-z0-9]+)(?::(.+))?$/i;
|
|
30
|
+
function parsePermission(raw) {
|
|
31
|
+
const m = PERMISSION_RE.exec(raw);
|
|
32
|
+
if (!m) return null;
|
|
33
|
+
const [, domain, action, resource] = m;
|
|
34
|
+
return resource === void 0 ? { domain, action } : { domain, action, resource };
|
|
35
|
+
}
|
|
36
|
+
function isPermissionGranted(required, granted) {
|
|
37
|
+
return granted.some((g) => {
|
|
38
|
+
if (g.domain !== required.domain || g.action !== required.action) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (required.resource === void 0) return true;
|
|
42
|
+
return g.resource === "*" || g.resource === required.resource;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function checkPermissions(required, grantedRaw) {
|
|
46
|
+
const grantedParsed = grantedRaw.map(parsePermission).filter((p) => p !== null);
|
|
47
|
+
const granted = [];
|
|
48
|
+
const missing = [];
|
|
49
|
+
for (const raw of required) {
|
|
50
|
+
const parsed = parsePermission(raw);
|
|
51
|
+
if (parsed && isPermissionGranted(parsed, grantedParsed)) {
|
|
52
|
+
granted.push(raw);
|
|
53
|
+
} else {
|
|
54
|
+
missing.push(raw);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { ok: missing.length === 0, granted, missing };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/testing/mock-host.ts
|
|
61
|
+
var DEFAULT_RESPONSE = { status: 200, headers: {}, body: {} };
|
|
62
|
+
function createMockHostApi(opts = {}) {
|
|
63
|
+
const calls = {
|
|
64
|
+
"http.request": [],
|
|
65
|
+
"secrets.get": [],
|
|
66
|
+
"storage.getUrl": [],
|
|
67
|
+
"storage.put": [],
|
|
68
|
+
"sequence.next": [],
|
|
69
|
+
"host.on": []
|
|
70
|
+
};
|
|
71
|
+
const registered = [];
|
|
72
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
73
|
+
const seqState = new Map(
|
|
74
|
+
Object.entries(opts.sequences ?? {})
|
|
75
|
+
);
|
|
76
|
+
const enforce = (required) => {
|
|
77
|
+
if (!opts.grantedPermissions) return;
|
|
78
|
+
const check = checkPermissions([required], opts.grantedPermissions);
|
|
79
|
+
if (!check.ok) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`MockHostApi: permission denied \u2014 plugin needs "${required}" but it is not in grantedPermissions. Declare it in your manifest.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const noop = () => {
|
|
86
|
+
};
|
|
87
|
+
const logger = {
|
|
88
|
+
debug: noop,
|
|
89
|
+
info: noop,
|
|
90
|
+
warn: noop,
|
|
91
|
+
error: noop
|
|
92
|
+
};
|
|
93
|
+
const http = {
|
|
94
|
+
// async so a denied-permission throw surfaces as a rejected promise, not a
|
|
95
|
+
// synchronous throw — matches the real host's async port contract.
|
|
96
|
+
request: async (req) => {
|
|
97
|
+
const hostname = safeHostname(req.url);
|
|
98
|
+
enforce(`http.outbound:${hostname}`);
|
|
99
|
+
calls["http.request"].push(req);
|
|
100
|
+
return opts.httpResponses?.[req.url] ?? Object.entries(opts.httpResponses ?? {}).find(
|
|
101
|
+
([k]) => req.url.includes(k)
|
|
102
|
+
)?.[1] ?? DEFAULT_RESPONSE;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const secrets = {
|
|
106
|
+
get: async (key) => {
|
|
107
|
+
enforce(`secrets.read:${key}`);
|
|
108
|
+
calls["secrets.get"].push(key);
|
|
109
|
+
return opts.secrets?.[key];
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const storage = {
|
|
113
|
+
getUrl: (key) => {
|
|
114
|
+
calls["storage.getUrl"].push(key);
|
|
115
|
+
return Promise.resolve(`mock://storage/${key}`);
|
|
116
|
+
},
|
|
117
|
+
put: (key, body) => {
|
|
118
|
+
calls["storage.put"].push({ key, body });
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const sequence = {
|
|
123
|
+
next: (scope) => {
|
|
124
|
+
calls["sequence.next"].push(scope);
|
|
125
|
+
const current = seqState.get(scope) ?? 0;
|
|
126
|
+
const next = current + 1;
|
|
127
|
+
seqState.set(scope, next);
|
|
128
|
+
return Promise.resolve(next);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const host = {
|
|
132
|
+
logger,
|
|
133
|
+
http,
|
|
134
|
+
secrets,
|
|
135
|
+
storage,
|
|
136
|
+
sequence,
|
|
137
|
+
calls,
|
|
138
|
+
registered,
|
|
139
|
+
handlers,
|
|
140
|
+
on: (event, handler) => {
|
|
141
|
+
calls["host.on"].push(event);
|
|
142
|
+
const list = handlers.get(event) ?? [];
|
|
143
|
+
list.push(handler);
|
|
144
|
+
handlers.set(event, list);
|
|
145
|
+
},
|
|
146
|
+
registerCapability: (cap) => {
|
|
147
|
+
registered.push(cap);
|
|
148
|
+
},
|
|
149
|
+
emit: async (event, payload) => {
|
|
150
|
+
for (const h of handlers.get(event) ?? []) await h(payload);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
return host;
|
|
154
|
+
}
|
|
155
|
+
function safeHostname(url) {
|
|
156
|
+
try {
|
|
157
|
+
return new URL(url).hostname;
|
|
158
|
+
} catch {
|
|
159
|
+
return url;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function createDispatchContext(config = {}, overrides = {}) {
|
|
163
|
+
return { config, ...overrides };
|
|
164
|
+
}
|
|
165
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
166
|
+
0 && (module.exports = {
|
|
167
|
+
createDispatchContext,
|
|
168
|
+
createMockHostApi
|
|
169
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkPermissions
|
|
3
|
+
} from "../chunk-DGJZW46B.mjs";
|
|
4
|
+
|
|
5
|
+
// src/testing/mock-host.ts
|
|
6
|
+
var DEFAULT_RESPONSE = { status: 200, headers: {}, body: {} };
|
|
7
|
+
function createMockHostApi(opts = {}) {
|
|
8
|
+
const calls = {
|
|
9
|
+
"http.request": [],
|
|
10
|
+
"secrets.get": [],
|
|
11
|
+
"storage.getUrl": [],
|
|
12
|
+
"storage.put": [],
|
|
13
|
+
"sequence.next": [],
|
|
14
|
+
"host.on": []
|
|
15
|
+
};
|
|
16
|
+
const registered = [];
|
|
17
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
18
|
+
const seqState = new Map(
|
|
19
|
+
Object.entries(opts.sequences ?? {})
|
|
20
|
+
);
|
|
21
|
+
const enforce = (required) => {
|
|
22
|
+
if (!opts.grantedPermissions) return;
|
|
23
|
+
const check = checkPermissions([required], opts.grantedPermissions);
|
|
24
|
+
if (!check.ok) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`MockHostApi: permission denied \u2014 plugin needs "${required}" but it is not in grantedPermissions. Declare it in your manifest.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const noop = () => {
|
|
31
|
+
};
|
|
32
|
+
const logger = {
|
|
33
|
+
debug: noop,
|
|
34
|
+
info: noop,
|
|
35
|
+
warn: noop,
|
|
36
|
+
error: noop
|
|
37
|
+
};
|
|
38
|
+
const http = {
|
|
39
|
+
// async so a denied-permission throw surfaces as a rejected promise, not a
|
|
40
|
+
// synchronous throw — matches the real host's async port contract.
|
|
41
|
+
request: async (req) => {
|
|
42
|
+
const hostname = safeHostname(req.url);
|
|
43
|
+
enforce(`http.outbound:${hostname}`);
|
|
44
|
+
calls["http.request"].push(req);
|
|
45
|
+
return opts.httpResponses?.[req.url] ?? Object.entries(opts.httpResponses ?? {}).find(
|
|
46
|
+
([k]) => req.url.includes(k)
|
|
47
|
+
)?.[1] ?? DEFAULT_RESPONSE;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const secrets = {
|
|
51
|
+
get: async (key) => {
|
|
52
|
+
enforce(`secrets.read:${key}`);
|
|
53
|
+
calls["secrets.get"].push(key);
|
|
54
|
+
return opts.secrets?.[key];
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const storage = {
|
|
58
|
+
getUrl: (key) => {
|
|
59
|
+
calls["storage.getUrl"].push(key);
|
|
60
|
+
return Promise.resolve(`mock://storage/${key}`);
|
|
61
|
+
},
|
|
62
|
+
put: (key, body) => {
|
|
63
|
+
calls["storage.put"].push({ key, body });
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const sequence = {
|
|
68
|
+
next: (scope) => {
|
|
69
|
+
calls["sequence.next"].push(scope);
|
|
70
|
+
const current = seqState.get(scope) ?? 0;
|
|
71
|
+
const next = current + 1;
|
|
72
|
+
seqState.set(scope, next);
|
|
73
|
+
return Promise.resolve(next);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const host = {
|
|
77
|
+
logger,
|
|
78
|
+
http,
|
|
79
|
+
secrets,
|
|
80
|
+
storage,
|
|
81
|
+
sequence,
|
|
82
|
+
calls,
|
|
83
|
+
registered,
|
|
84
|
+
handlers,
|
|
85
|
+
on: (event, handler) => {
|
|
86
|
+
calls["host.on"].push(event);
|
|
87
|
+
const list = handlers.get(event) ?? [];
|
|
88
|
+
list.push(handler);
|
|
89
|
+
handlers.set(event, list);
|
|
90
|
+
},
|
|
91
|
+
registerCapability: (cap) => {
|
|
92
|
+
registered.push(cap);
|
|
93
|
+
},
|
|
94
|
+
emit: async (event, payload) => {
|
|
95
|
+
for (const h of handlers.get(event) ?? []) await h(payload);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return host;
|
|
99
|
+
}
|
|
100
|
+
function safeHostname(url) {
|
|
101
|
+
try {
|
|
102
|
+
return new URL(url).hostname;
|
|
103
|
+
} catch {
|
|
104
|
+
return url;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function createDispatchContext(config = {}, overrides = {}) {
|
|
108
|
+
return { config, ...overrides };
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
createDispatchContext,
|
|
112
|
+
createMockHostApi
|
|
113
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vivoa/plugin-sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Public contracts for Vivoa plugins (types, DI tokens, config + setup-layout validators)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vivoa",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"import": "./dist/index.mjs",
|
|
26
26
|
"require": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./testing": {
|
|
29
|
+
"types": "./dist/testing/index.d.ts",
|
|
30
|
+
"import": "./dist/testing/index.mjs",
|
|
31
|
+
"require": "./dist/testing/index.js"
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
"sideEffects": false,
|
|
@@ -44,8 +49,8 @@
|
|
|
44
49
|
"vitest": "^2.1.8"
|
|
45
50
|
},
|
|
46
51
|
"scripts": {
|
|
47
|
-
"build": "tsup src/index.ts --format cjs,esm --dts --clean --target node18",
|
|
48
|
-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --target node18",
|
|
52
|
+
"build": "tsup src/index.ts src/testing/index.ts --format cjs,esm --dts --clean --target node18",
|
|
53
|
+
"dev": "tsup src/index.ts src/testing/index.ts --format cjs,esm --dts --watch --target node18",
|
|
49
54
|
"test": "vitest run",
|
|
50
55
|
"test:watch": "vitest",
|
|
51
56
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|