@crowi/plugin-api 0.1.0-alpha.2 → 1.0.0-alpha.11
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 +151 -0
- package/dist/index.d.mts +940 -118
- package/dist/index.d.ts +940 -118
- package/dist/index.js +360 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +343 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -5
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @crowi/plugin-api
|
|
2
|
+
|
|
3
|
+
Type-only contract for Crowi 2.0 plugins. A Crowi plugin is an ordinary
|
|
4
|
+
npm package that default-exports an object satisfying the `CrowiPlugin`
|
|
5
|
+
type from this package; the runtime loads it via `await
|
|
6
|
+
import('<plugin-name>')` at boot and calls each `register*` callback it
|
|
7
|
+
implements. See
|
|
8
|
+
[RFC-0001](https://github.com/crowi/crowi/blob/main/docs/rfcs/0001-plugin-architecture.md)
|
|
9
|
+
for the full design and the
|
|
10
|
+
[plugin development guide](https://crowi.wiki/en/docs/develop/plugins-developing)
|
|
11
|
+
for a walkthrough.
|
|
12
|
+
|
|
13
|
+
> **Alpha notice**: Crowi v2 is under active alpha development and this
|
|
14
|
+
> package is at `0.x`. The API stability guarantee (working across every
|
|
15
|
+
> v2 minor) takes effect once it reaches `2.x`; the contract may still
|
|
16
|
+
> change during the alpha period.
|
|
17
|
+
|
|
18
|
+
## Minimal plugin
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import type { CrowiPlugin } from '@crowi/plugin-api';
|
|
22
|
+
|
|
23
|
+
const myPlugin: CrowiPlugin = {
|
|
24
|
+
name: '@example/crowi-plugin-mystorage',
|
|
25
|
+
version: '0.1.0',
|
|
26
|
+
|
|
27
|
+
registerStorage: (registry, ctx) => {
|
|
28
|
+
registry.register('mystorage', {
|
|
29
|
+
put: async (key, body, meta) => ({ key }),
|
|
30
|
+
get: async (key) => { throw new Error('not implemented'); },
|
|
31
|
+
delete: async (key) => {},
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default myPlugin;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`name` must match the npm package name — it doubles as the namespace
|
|
40
|
+
prefix for this plugin's config rows (`plugin:<name>:*`) and per-page
|
|
41
|
+
metadata (`page.metadata['<name>']`).
|
|
42
|
+
|
|
43
|
+
## Authoring a config schema
|
|
44
|
+
|
|
45
|
+
A plugin declares its configurable values with `configSchema`, a Zod
|
|
46
|
+
object schema. The admin UI at `/admin/plugins` walks this schema to
|
|
47
|
+
auto-generate a config form, encrypt `@sensitive`-marked fields at
|
|
48
|
+
rest, and render `@action`-marked fields with a button.
|
|
49
|
+
|
|
50
|
+
This package's `peerDependencies` declares `zod: "^4"` — that is the
|
|
51
|
+
correct **npm package** to install, because the zod v4 package is the
|
|
52
|
+
one that ships a `zod/v3` compat subpath. What `peerDependencies`
|
|
53
|
+
cannot express is *which entry point* to import from, and that part
|
|
54
|
+
matters:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// Correct — the v3 compat shim that the v4 package ships.
|
|
58
|
+
import { z } from 'zod/v3';
|
|
59
|
+
|
|
60
|
+
// Wrong — compiles and type-checks, but fails at plugin boot.
|
|
61
|
+
import { z } from 'zod';
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`CrowiPlugin.configSchema` is typed against `zod/v3`'s `z.ZodObject`,
|
|
65
|
+
and every runtime helper that reads a plugin's config schema
|
|
66
|
+
(`@sensitive` / `@action` marker detection, the admin form field
|
|
67
|
+
serializer, `listSensitiveKeys()`) introspects the `zod/v3` internal
|
|
68
|
+
shape (`_def.typeName`, `_def.values`, `.description`, …). A schema
|
|
69
|
+
built from the top-level `zod` (v4) API has a different internal shape
|
|
70
|
+
and none of that introspection can see through it — most importantly,
|
|
71
|
+
`@sensitive` fields stop being detected, and the value they guard would
|
|
72
|
+
be written to and read from storage as plaintext.
|
|
73
|
+
|
|
74
|
+
To turn that silent failure mode into a loud one, `PluginManager`
|
|
75
|
+
validates every plugin's `configSchema` at boot and throws if it wasn't
|
|
76
|
+
built from `zod/v3`:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Plugin '<name>' declares configSchema built with the top-level 'zod' (v4) API.
|
|
80
|
+
Import from 'zod/v3' instead — @crowi/plugin-api's config-schema introspection
|
|
81
|
+
requires the zod v3 compat shape (see @crowi/plugin-api README).
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If you hit this error, the fix is always the same: change the `zod`
|
|
85
|
+
import for the file that builds `configSchema` (and `pageMetadataSchema`,
|
|
86
|
+
if you use it) to `import { z } from 'zod/v3'`. No other code needs to
|
|
87
|
+
change — `zod/v3`'s `z.object()` / `z.string()` / etc. API surface is
|
|
88
|
+
what every first-party Crowi plugin already uses.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { z } from 'zod/v3';
|
|
92
|
+
import type { CrowiPlugin } from '@crowi/plugin-api';
|
|
93
|
+
|
|
94
|
+
const myPlugin: CrowiPlugin = {
|
|
95
|
+
name: '@example/crowi-plugin-mystorage',
|
|
96
|
+
version: '0.1.0',
|
|
97
|
+
|
|
98
|
+
configSchema: z.object({
|
|
99
|
+
endpoint: z.string().url().describe('Storage endpoint URL'),
|
|
100
|
+
accessKey: z.string().describe('@sensitive Access key'),
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
registerStorage: (registry, ctx) => {
|
|
104
|
+
const config = ctx.config<{ endpoint: string; accessKey: string }>();
|
|
105
|
+
// ... build the driver using config
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default myPlugin;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Post-save connectivity verification (`verifyConfig`)
|
|
113
|
+
|
|
114
|
+
A plugin whose config change needs a real connectivity/permission check (a storage bucket, a search cluster, …) can implement `verifyConfig`. The runtime calls it once after an admin save has already persisted and `reconfigure` has already run — never before, and never as a condition for the save itself:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import type { CrowiPlugin, PluginConfigVerificationSnapshot, PluginConfigVerificationOptions, PluginConfigVerificationResult } from '@crowi/plugin-api';
|
|
118
|
+
|
|
119
|
+
const myPlugin: CrowiPlugin = {
|
|
120
|
+
// ...
|
|
121
|
+
|
|
122
|
+
verifyConfig: async (
|
|
123
|
+
snapshot: PluginConfigVerificationSnapshot,
|
|
124
|
+
options: PluginConfigVerificationOptions,
|
|
125
|
+
): Promise<PluginConfigVerificationResult> => {
|
|
126
|
+
const config = snapshot.config<{ endpoint: string; accessKey: string }>();
|
|
127
|
+
try {
|
|
128
|
+
await probeMyBackend(config);
|
|
129
|
+
return { status: 'ok' };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { status: 'failed', reason: classifyMyError(err) };
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A few things make this different from every other `register*` / `reconfigure` callback:
|
|
138
|
+
|
|
139
|
+
- **Snapshot, not `PluginContext`.** `verifyConfig` receives a `PluginConfigVerificationSnapshot` — a read-only, point-in-time view of this plugin's own config (and any declared, `exposesConfigToDependents` dependency's config), frozen at the moment the triggering save was about to persist. It is NOT the live `PluginContext`: there is no `setConfig`, `model`, `state`, or `pageMetadata` on it, and calling `snapshot.config()` later never reflects a different admin request's save that lands while your hook is still running.
|
|
140
|
+
- **Fans out to dependents.** If plugin B `requires` plugin A and B implements `verifyConfig`, saving A's config also re-verifies B (same affected-set walk `reconfigure` uses). B's hook only sees A's dependency config if A also set `exposesConfigToDependents: true`.
|
|
141
|
+
- **Non-blocking, always.** A failing (or throwing, or never-resolving) `verifyConfig` never fails the save — the save already succeeded by the time this hook runs. `options.timeoutMs` (currently 10 seconds) is a NOTICE the caller stops waiting on your promise after, not a cancellation signal: there is no `AbortSignal` anywhere in this contract, and none is threaded down into any `StorageDriver` call your hook makes. Design your hook's own I/O with a bounded retry/attempt policy (e.g. a single attempt, no retries) so it settles well within that budget on its own.
|
|
142
|
+
- **Result is a closed, safe union.** Return `{ status: 'ok' }` or `{ status: 'failed', reason }`, where `reason` is one of `'unreachable' | 'auth-failed' | 'resource-missing' | 'write-denied' | 'unknown'`. Never put raw SDK error text, a stack trace, an endpoint, or credential material anywhere in the result (or in anything you log) — the runtime reports this straight to the admin API response. Anything your hook returns outside this shape is normalized to `{ status: 'failed', reason: 'unknown' }` by the caller, so prefer an honest `'unknown'` yourself over guessing a more specific reason you can't actually confirm.
|
|
143
|
+
- **Optional.** A plugin with no `verifyConfig` is completely unaffected — no extra work at boot or save time, no entry in the response's `verificationResults`.
|
|
144
|
+
- **Instance-local, not cluster-wide.** The runtime calls `verifyConfig` on whichever api process handled the save request and reports only that process's outcome. It never coordinates with other replicas, so a result reflects reachability/permissions from that one instance at that moment — not the deployment as a whole. If your hook's I/O (network reachability, IAM/role assumption, DNS) can differ between replicas, document that for operators; don't imply a passing result means every replica can reach the backend.
|
|
145
|
+
|
|
146
|
+
## See also
|
|
147
|
+
|
|
148
|
+
- [Plugin development guide](https://crowi.wiki/en/docs/develop/plugins-developing) —
|
|
149
|
+
the full walkthrough (markers, `adminPlacement`, `configI18n`,
|
|
150
|
+
`PluginContext`, dependency plugins, renderer plugins).
|
|
151
|
+
- [RFC-0001: Plugin architecture](https://github.com/crowi/crowi/blob/main/docs/rfcs/0001-plugin-architecture.md)
|