@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 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, `IPaymentPlugin`/`IShippingPlugin`, config, setup
11
- > layout, permissions, build/publish, and the host landmines to avoid).
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
- ## What's inside
19
+ ## Capabilities
20
20
 
21
- | Module | Exports |
22
- | ---------------- | ----------------------------------------------------------------------- |
23
- | `types` | `IPlugin`, `IPaymentPlugin`, `IShippingPlugin`, payment/shipping models, DI tokens |
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
- All exported from the package root:
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
- validateSetupLayout,
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
- ## Defining a configurable plugin
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: field({ label: 'API Key', type: 'password' }),
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; // -> PluginConfigField[] for the admin UI
50
- config.validate(input); // -> { success, data | errors }
185
+ config.configFields; // PluginConfigField[] → rendered by the admin UI
186
+ config.validate(userInput); // { success, data } | { success: false, errors }
51
187
  ```
52
188
 
53
- ## Validating a setup layout
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. Breaking changes to any exported type or validator bump the major.
67
- Plugins should pin a caret range (`^0.1.0`).
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
+ };