@vivoa/plugin-sdk 0.6.3 → 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/README.md +178 -18
- 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 +6 -1164
- package/dist/index.d.ts +6 -1164
- package/dist/index.js +72 -2
- package/dist/index.mjs +71 -34
- 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/README.md
CHANGED
|
@@ -7,8 +7,8 @@ A plugin author depends on this package; the Vivoa host re-exports it as its
|
|
|
7
7
|
single source of truth for plugin shapes.
|
|
8
8
|
|
|
9
9
|
> **Building a plugin?** Read [`AUTHORING.md`](./AUTHORING.md) — the complete,
|
|
10
|
-
> AI-legible guide (anatomy,
|
|
11
|
-
>
|
|
10
|
+
> AI-legible guide (anatomy, capabilities, config, setup layout, permissions,
|
|
11
|
+
> build/publish, and the host landmines to avoid).
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -16,41 +16,177 @@ single source of truth for plugin shapes.
|
|
|
16
16
|
npm install @vivoa/plugin-sdk
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Capabilities
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
| `manifest` | `PluginManifest` |
|
|
25
|
-
| `config-schema` | `createPluginConfig`, `field` — declarative config + superstruct validation |
|
|
26
|
-
| `setup-layout` | `SetupLayout` types, `validateSetupLayout`, `crossValidateSetupLayout` |
|
|
21
|
+
A plugin is a **factory function** `(host: HostApi) => Plugin` that declares
|
|
22
|
+
capabilities instead of extending a NestJS module. The host routes by
|
|
23
|
+
`CapabilityKind` string — adding a new kind never touches host-core code.
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
| `CapabilityKind` | Interface | Helper |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `payment` | `IPaymentPlugin` | `paymentCapability(impl)` |
|
|
28
|
+
| `shipping` | `IShippingPlugin` | `shippingCapability(impl)` |
|
|
29
|
+
| `suppliers` | `ISuppliersPlugin` | `suppliersCapability(impl)` |
|
|
30
|
+
| `analytics` | `IAnalyticsPlugin` | `analyticsCapability(impl)` |
|
|
31
|
+
| `invoicing` | `IInvoicingPlugin` | `invoicingCapability(impl)` |
|
|
32
|
+
|
|
33
|
+
A plugin may expose any combination of capabilities (or a custom `kind`).
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
29
36
|
|
|
30
37
|
```ts
|
|
31
38
|
import {
|
|
39
|
+
definePlugin,
|
|
40
|
+
paymentCapability,
|
|
32
41
|
IPaymentPlugin,
|
|
33
42
|
createPluginConfig,
|
|
34
43
|
field,
|
|
35
|
-
|
|
44
|
+
} from '@vivoa/plugin-sdk';
|
|
45
|
+
|
|
46
|
+
const config = createPluginConfig({
|
|
47
|
+
apiKey: field({ label: 'API Key', type: 'password' }),
|
|
48
|
+
sandbox: field({ label: 'Sandbox mode', type: 'boolean', required: false }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const myPaymentPlugin: IPaymentPlugin = {
|
|
52
|
+
name: 'my-gateway',
|
|
53
|
+
paymentConfig: {
|
|
54
|
+
supportedCountries: ['SV'],
|
|
55
|
+
configSchema: config.configFields,
|
|
56
|
+
supportedCurrencies: ['USD'],
|
|
57
|
+
},
|
|
58
|
+
async createPaymentSession(req, ctx) { /* ... */ },
|
|
59
|
+
async processPayment(req, ctx) { /* ... */ },
|
|
60
|
+
async refundPayment(req, ctx) { /* ... */ },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default definePlugin((host) => ({
|
|
64
|
+
manifest: { code: 'my-gateway', name: 'My Gateway', sdkVersion: SDK_VERSION },
|
|
65
|
+
capabilities: [paymentCapability(myPaymentPlugin)],
|
|
66
|
+
onActivate(host) {
|
|
67
|
+
host.logger.info('my-gateway activated');
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Modules
|
|
73
|
+
|
|
74
|
+
| Module | Key exports |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `plugin` | `IPlugin` |
|
|
77
|
+
| `capability` | `Capability`, `CapabilityKind`, `definePlugin`, `Plugin`, `PluginFactory`, `paymentCapability`, `shippingCapability`, `suppliersCapability`, `analyticsCapability`, `invoicingCapability` |
|
|
78
|
+
| `host-api` | `HostApi`, `Logger`, `HttpClient`, `SecretStore`, `StorageBroker`, `SequenceStore` |
|
|
79
|
+
| `payment` | `IPaymentPlugin`, `PaymentSession`, payment models |
|
|
80
|
+
| `shipping` | `IShippingPlugin`, `ShippingQuote`, shipping models |
|
|
81
|
+
| `suppliers` | `ISuppliersPlugin`, suppliers models |
|
|
82
|
+
| `analytics` | `IAnalyticsPlugin`, `AnalyticsEvent`, `AnalyticsUserData`, `AnalyticsActionSource` |
|
|
83
|
+
| `invoicing` | `IInvoicingPlugin`, `InvoiceRequest`, `InvoiceResult`, `InvoiceStatus`, `InvoiceDocumentType`, `InvoicingFeature` |
|
|
84
|
+
| `manifest` | `PluginManifest` |
|
|
85
|
+
| `config` | `PluginConfigField` |
|
|
86
|
+
| `config-schema` | `createPluginConfig`, `field` — declarative config + superstruct validation |
|
|
87
|
+
| `setup-layout` | `SetupLayout` types, `validateSetupLayout`, `crossValidateSetupLayout` |
|
|
88
|
+
| `permissions` | `Permission`, `parsePermission`, `isPermissionGranted`, `checkPermissions` |
|
|
89
|
+
| `dispatch` | `DispatchContext` |
|
|
90
|
+
| `tokens` | DI tokens (`PAYMENT_PLUGIN_TOKEN`, `SHIPPING_PLUGIN_TOKEN`, ...) |
|
|
91
|
+
| `version` | `SDK_VERSION`, `isSdkCompatible` |
|
|
92
|
+
|
|
93
|
+
All exported from the package root:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import {
|
|
97
|
+
definePlugin,
|
|
98
|
+
paymentCapability,
|
|
99
|
+
invoicingCapability,
|
|
100
|
+
createPluginConfig,
|
|
101
|
+
field,
|
|
102
|
+
SDK_VERSION,
|
|
36
103
|
} from '@vivoa/plugin-sdk';
|
|
37
104
|
```
|
|
38
105
|
|
|
39
|
-
##
|
|
106
|
+
## HostApi
|
|
107
|
+
|
|
108
|
+
The factory receives a `HostApi` — the plugin's entire window into the host.
|
|
109
|
+
No `@nestjs/*`, no Prisma, no `process.env`.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
interface HostApi {
|
|
113
|
+
readonly logger: Logger; // structured logging
|
|
114
|
+
readonly http: HttpClient; // outbound HTTP (permission-gated)
|
|
115
|
+
readonly secrets: SecretStore; // per-tenant secrets
|
|
116
|
+
readonly storage: StorageBroker; // object storage (presigned)
|
|
117
|
+
readonly sequence: SequenceStore; // monotonic counters (e.g. audit_no)
|
|
118
|
+
on(event: string, handler: DomainEventHandler): void;
|
|
119
|
+
registerCapability<T>(cap: Capability<T>): void;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## DispatchContext
|
|
124
|
+
|
|
125
|
+
Every interface method receives a `DispatchContext` — the host resolves it per
|
|
126
|
+
call, per tenant. The plugin holds **no per-tenant state**.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
interface DispatchContext {
|
|
130
|
+
config: Record<string, unknown>; // merged global config
|
|
131
|
+
countryConfig?: Record<string, unknown>; // country-scoped config
|
|
132
|
+
countryConfigs?: Record<string, Record<string, unknown>>;
|
|
133
|
+
countryCode?: string;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Invoicing capability (0.6.4)
|
|
138
|
+
|
|
139
|
+
Country-agnostic electronic-invoicing (DTE / CFDI / …) contract.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import {
|
|
143
|
+
definePlugin,
|
|
144
|
+
invoicingCapability,
|
|
145
|
+
IInvoicingPlugin,
|
|
146
|
+
InvoicingFeature,
|
|
147
|
+
InvoiceDocumentType,
|
|
148
|
+
InvoiceStatus,
|
|
149
|
+
} from '@vivoa/plugin-sdk';
|
|
150
|
+
|
|
151
|
+
const myInvoicingAdapter: IInvoicingPlugin = {
|
|
152
|
+
name: 'my-dte-adapter',
|
|
153
|
+
invoicingConfig: {
|
|
154
|
+
supportedCountries: ['SV'],
|
|
155
|
+
supportedFeatures: [InvoicingFeature.Issue, InvoicingFeature.Void],
|
|
156
|
+
supportedDocumentTypes: [
|
|
157
|
+
InvoiceDocumentType.Invoice,
|
|
158
|
+
InvoiceDocumentType.CreditInvoice,
|
|
159
|
+
],
|
|
160
|
+
configSchema: [],
|
|
161
|
+
},
|
|
162
|
+
async issueInvoice(req, ctx) {
|
|
163
|
+
// call provider API, return InvoiceResult
|
|
164
|
+
return { status: InvoiceStatus.Accepted, generationCode: '...' };
|
|
165
|
+
},
|
|
166
|
+
async voidInvoice(input, ctx) { /* ... */ },
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default definePlugin((host) => ({
|
|
170
|
+
manifest: { code: 'my-dte', name: 'My DTE', sdkVersion: SDK_VERSION },
|
|
171
|
+
capabilities: [invoicingCapability(myInvoicingAdapter)],
|
|
172
|
+
}));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Declarative config
|
|
40
176
|
|
|
41
177
|
```ts
|
|
42
178
|
import { createPluginConfig, field } from '@vivoa/plugin-sdk';
|
|
43
179
|
|
|
44
180
|
const config = createPluginConfig({
|
|
45
|
-
apiKey:
|
|
181
|
+
apiKey: field({ label: 'API Key', type: 'password' }),
|
|
46
182
|
sandbox: field({ label: 'Sandbox', type: 'boolean', required: false }),
|
|
47
183
|
});
|
|
48
184
|
|
|
49
|
-
config.configFields;
|
|
50
|
-
config.validate(
|
|
185
|
+
config.configFields; // PluginConfigField[] → rendered by the admin UI
|
|
186
|
+
config.validate(userInput); // { success, data } | { success: false, errors }
|
|
51
187
|
```
|
|
52
188
|
|
|
53
|
-
##
|
|
189
|
+
## Setup-layout validation
|
|
54
190
|
|
|
55
191
|
```ts
|
|
56
192
|
import { validateSetupLayout, crossValidateSetupLayout } from '@vivoa/plugin-sdk';
|
|
@@ -61,10 +197,34 @@ if (!res.success) throw new Error(res.errors.join('\n'));
|
|
|
61
197
|
const keyErrors = crossValidateSetupLayout(res.data, config.configFields);
|
|
62
198
|
```
|
|
63
199
|
|
|
200
|
+
## Permissions
|
|
201
|
+
|
|
202
|
+
Plugins declare `permissions: string[]` in their manifest. The host enforces
|
|
203
|
+
each mediated port before it executes.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// domain.action or domain.action:resource
|
|
207
|
+
const required = parsePermission('http.outbound:api.provider.com');
|
|
208
|
+
const ok = isPermissionGranted(required, grantedList);
|
|
209
|
+
```
|
|
210
|
+
|
|
64
211
|
## Versioning
|
|
65
212
|
|
|
66
|
-
Semver.
|
|
67
|
-
|
|
213
|
+
Semver. `SDK_VERSION` constant mirrors `package.json`. Use `isSdkCompatible` in
|
|
214
|
+
the host to check plugin compatibility.
|
|
215
|
+
|
|
216
|
+
**0.x convention:** the minor acts as the breaking boundary (matching
|
|
217
|
+
`^0.x` semantics). Additive changes ship as a **PATCH** to keep 0.x plugins
|
|
218
|
+
compatible across the same minor.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { SDK_VERSION, isSdkCompatible } from '@vivoa/plugin-sdk';
|
|
222
|
+
|
|
223
|
+
// in the host loader:
|
|
224
|
+
if (!isSdkCompatible(plugin.manifest.sdkVersion, SDK_VERSION)) {
|
|
225
|
+
throw new Error('Incompatible plugin SDK version');
|
|
226
|
+
}
|
|
227
|
+
```
|
|
68
228
|
|
|
69
229
|
## License
|
|
70
230
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/permissions.ts
|
|
2
|
+
var PERMISSION_RE = /^([a-z0-9]+)\.([a-z0-9]+)(?::(.+))?$/i;
|
|
3
|
+
function parsePermission(raw) {
|
|
4
|
+
const m = PERMISSION_RE.exec(raw);
|
|
5
|
+
if (!m) return null;
|
|
6
|
+
const [, domain, action, resource] = m;
|
|
7
|
+
return resource === void 0 ? { domain, action } : { domain, action, resource };
|
|
8
|
+
}
|
|
9
|
+
function isPermissionGranted(required, granted) {
|
|
10
|
+
return granted.some((g) => {
|
|
11
|
+
if (g.domain !== required.domain || g.action !== required.action) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (required.resource === void 0) return true;
|
|
15
|
+
return g.resource === "*" || g.resource === required.resource;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function checkPermissions(required, grantedRaw) {
|
|
19
|
+
const grantedParsed = grantedRaw.map(parsePermission).filter((p) => p !== null);
|
|
20
|
+
const granted = [];
|
|
21
|
+
const missing = [];
|
|
22
|
+
for (const raw of required) {
|
|
23
|
+
const parsed = parsePermission(raw);
|
|
24
|
+
if (parsed && isPermissionGranted(parsed, grantedParsed)) {
|
|
25
|
+
granted.push(raw);
|
|
26
|
+
} else {
|
|
27
|
+
missing.push(raw);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { ok: missing.length === 0, granted, missing };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
parsePermission,
|
|
35
|
+
isPermissionGranted,
|
|
36
|
+
checkPermissions
|
|
37
|
+
};
|