@crowi/plugin-api 0.1.0-alpha.1 → 0.1.0-alpha.2

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/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { Readable } from 'node:stream';
3
+ import { Context } from 'hono';
3
4
 
4
5
  /**
5
6
  * The context object passed to every plugin callback. It is the only
@@ -29,6 +30,13 @@ interface PluginContext {
29
30
  * instead of duplicating the fields in their own configSchema.
30
31
  */
31
32
  dependencyConfig<T>(dependencyName: string): T;
33
+ /**
34
+ * Read core application info (the wiki name, …) — settings that live
35
+ * outside this plugin's own config namespace but that an integration
36
+ * may need (e.g. to brand an outbound manifest). Read live at call
37
+ * time, so it reflects admin edits made after boot.
38
+ */
39
+ appInfo(): AppInfo;
32
40
  /** Write a single config field, persisting to Mongo. */
33
41
  setConfig(key: string, value: unknown): Promise<void>;
34
42
  /** Per-Page metadata accessor for this plugin's namespace. */
@@ -48,6 +56,30 @@ interface PluginContext {
48
56
  /** Structured logger scoped to this plugin (auto-prefixed with name). */
49
57
  log: PluginLogger;
50
58
  }
59
+ /**
60
+ * Read-only view of core application settings exposed to plugins via
61
+ * `ctx.appInfo()`. Intentionally a small, curated surface (not a generic
62
+ * "read any core config" escape hatch) — add fields here as concrete
63
+ * plugin needs appear.
64
+ */
65
+ interface AppInfo {
66
+ /**
67
+ * The configured wiki name (core `app:title`), trimmed. Always a
68
+ * non-empty string: when the operator has not set a custom title it
69
+ * defaults to `'Crowi'` (the seed value), so consumers never have to
70
+ * handle an absent name.
71
+ */
72
+ title: string;
73
+ /**
74
+ * The wiki's public base origin (core `CLIENT_URL` / `getBaseUrl()`),
75
+ * e.g. `https://wiki.example.com`. An **empty string** when no public
76
+ * origin is configured — unlike `title` there is no sensible default,
77
+ * so a plugin that needs an absolute URL (outbound webhook / manifest)
78
+ * must handle the empty case. Plugins read this instead of
79
+ * `process.env.CLIENT_URL` directly.
80
+ */
81
+ baseUrl: string;
82
+ }
51
83
  /**
52
84
  * Per-Page metadata read / write helper. Each plugin gets a private
53
85
  * namespace at `page.metadata['<plugin-name>']`; this accessor scopes
@@ -910,28 +942,65 @@ interface RendererRegistry {
910
942
  }
911
943
 
912
944
  /**
913
- * Scope passed to `registerRoutes`. The HTTP route contribution surface
914
- * for plugins is **not currently wired** to the runtime — it has never
915
- * been invoked end-to-end. RFC-0006 Phase 6 removed the framework
916
- * dependency that previously typed the contract argument; the API
917
- * surface is therefore a deliberate no-op stub until a follow-up RFC
918
- * redesigns plugin HTTP contribution on top of Hono.
945
+ * HTTP method a plugin route can be mounted on. Kept to the verbs the
946
+ * inbound-webhook + admin-action surface actually needs (RFC-0013 §4):
947
+ * `POST` for Slack events / slash / interactivity + `@action` targets,
948
+ * `GET` for OAuth callbacks + simple status endpoints.
949
+ */
950
+ type PluginRouteMethod = 'GET' | 'POST';
951
+ /**
952
+ * A plugin route handler. It receives the raw Hono `Context` and returns
953
+ * a `Response` (or a promise of one), exactly like a hand-written Hono
954
+ * handler — the scope does **not** wrap it in a typed-route/validator
955
+ * layer.
919
956
  *
920
- * Plugins that declare a `registerRoutes` callback today see it
921
- * receive this scope and call `scope.register(...)` with arbitrary
922
- * arguments both arguments are accepted as `unknown` and the call
923
- * is silently dropped. The type fixture exists so the public surface
924
- * of `@crowi/plugin-api` keeps compiling against existing plugin
925
- * sources (including the in-tree `__fixtures__/example-plugin.ts`)
926
- * without forcing every plugin to be updated in lockstep with Phase 6.
957
+ * **Raw body invariant** (RFC-0013 §8, a Slack hard requirement): the
958
+ * route is a plain Hono route, NOT a `@hono/zod-openapi` route, so no
959
+ * body-consuming validator runs ahead of the handler. `c.req.text()` /
960
+ * `c.req.raw` therefore yield the *exact* bytes the client sent, which
961
+ * the Slack signature check (`HMAC-SHA256` over `v0:{ts}:{rawBody}`)
962
+ * depends on. `createJwtAuth` (installed on non-public routes) never
963
+ * reads the body, so the invariant holds for authed routes too.
964
+ */
965
+ type PluginRouteHandler = (c: Context) => Response | Promise<Response>;
966
+ /** Per-route options passed alongside the handler. */
967
+ interface PluginRouteOptions {
968
+ /**
969
+ * When `true`, the route is mounted **without** `createJwtAuth`, so it
970
+ * is reachable by unauthenticated requests (Crowi-auth public). Use for
971
+ * inbound webhooks that authenticate themselves out-of-band — e.g. the
972
+ * Slack Events API endpoint, which is gated by Slack's request-signature
973
+ * check rather than a Crowi session (RFC-0013 §8).
974
+ *
975
+ * Omitted / `false` mounts the route under `createJwtAuth`, so it
976
+ * requires a valid Crowi JWT just like a core authenticated endpoint
977
+ * (admin "Test connection" / `@action` targets, OAuth callbacks).
978
+ */
979
+ public?: boolean;
980
+ }
981
+ /**
982
+ * Scope passed to `registerRoutes(scope, ctx)`. Lets a plugin contribute
983
+ * HTTP routes that the runtime mounts at
984
+ * `/api/v2/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
985
+ * segment guarantees that core endpoints and other plugins cannot
986
+ * collide (RFC-0013 §4).
987
+ *
988
+ * The scope is built per-plugin inside `buildHonoApp` (the Hono app does
989
+ * not exist yet when plugins activate at boot), so `<plugin-name>` is
990
+ * already closed over — plugins only supply the sub-path.
927
991
  */
928
992
  interface PluginRouterScope {
929
993
  /**
930
- * No-op register. Both arguments are typed as `unknown` because the
931
- * runtime never reads them; concrete shapes are reserved for the
932
- * follow-up RFC that wires plugin HTTP contribution onto Hono.
994
+ * Mount `handler` for `method` at `<path>` under this plugin's
995
+ * namespace. `path` is relative to `/api/v2/plugins/<plugin-name>` and
996
+ * should start with `/` (e.g. `route('POST', '/events', handler, {
997
+ * public: true })` → `POST /api/v2/plugins/<name>/events`).
998
+ *
999
+ * Pass `{ public: true }` to bypass `createJwtAuth` for self-
1000
+ * authenticating inbound webhooks; omit it for routes that require a
1001
+ * Crowi session.
933
1002
  */
934
- register(contract: unknown, implementation: unknown): void;
1003
+ route(method: PluginRouteMethod, path: string, handler: PluginRouteHandler, opts?: PluginRouteOptions): void;
935
1004
  }
936
1005
 
937
1006
  /**
@@ -1002,7 +1071,7 @@ interface CrowiPlugin {
1002
1071
  * from a fixed allow-list to keep the bundle small.
1003
1072
  */
1004
1073
  adminPlacement?: {
1005
- section?: 'settings' | 'shared' | 'storage' | 'mail' | 'notification' | 'auth' | 'search' | 'renderer';
1074
+ section?: 'settings' | 'shared' | 'storage' | 'mail' | 'notification' | 'auth' | 'search' | 'renderer' | 'platform';
1006
1075
  label?: string;
1007
1076
  icon?: string;
1008
1077
  };
@@ -1052,13 +1121,24 @@ interface CrowiPlugin {
1052
1121
  */
1053
1122
  registerHooks?: (events: EventBus, ctx: PluginContext) => void;
1054
1123
  /**
1055
- * ts-rest contract that the plugin contributes. Mounted at
1056
- * `/api/v2/plugins/<name>/*` (the `<name>` path segment guarantees
1124
+ * HTTP routes the plugin contributes, mounted at
1125
+ * `/api/v2/plugins/<name>/<path>` (the `<name>` path segment guarantees
1057
1126
  * that core endpoints and other plugins cannot collide). Used for
1058
- * "Test connection" buttons, OAuth callbacks, custom admin views,
1059
- * etc. The contract surface uses ts-rest so the admin UI can call
1060
- * plugin endpoints with the same `apiClient.<plugin>.<method>` shape
1061
- * it uses for core endpoints.
1127
+ * inbound webhooks (Slack events / slash / interactivity), "Test
1128
+ * connection" buttons, `@action` targets, OAuth callbacks, etc.
1129
+ *
1130
+ * Each route is a plain Hono handler — `scope.route(method, path,
1131
+ * (c) => Response, opts?)`. The handler receives the raw `Context`, so
1132
+ * `c.req.text()` / `c.req.raw` give the exact request bytes (no
1133
+ * validator consumes the body ahead of it — the Slack signature check
1134
+ * relies on this). Pass `{ public: true }` to bypass `createJwtAuth`
1135
+ * for self-authenticating webhooks; omit it for Crowi-session-gated
1136
+ * routes.
1137
+ *
1138
+ * Called once at boot — but unlike the other `register*` hooks, this
1139
+ * runs inside `buildHonoApp` (the Hono app does not exist yet when
1140
+ * plugins activate), so a plugin's `registerRoutes` fires slightly
1141
+ * later than its `registerStorage` / `registerNotifier` / etc.
1062
1142
  */
1063
1143
  registerRoutes?: (scope: PluginRouterScope, ctx: PluginContext) => void;
1064
1144
  /**
@@ -1156,4 +1236,4 @@ interface ActionAnnotation {
1156
1236
  */
1157
1237
  declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
1158
1238
 
1159
- export { ACTION_FIELD_MARKER, type AuthContext, type AuthDriver, type AuthProfile, type AuthRegistry, type AuthVerifyResult, type CacheEntry, type CacheKey, type CacheStorage, type CodeBlockInfo, type CodeBlockRenderer, type CrowiPlugin, type EmailMessage, type EmbedFragment, type EmbedInput, type EmbedRenderer, type EventBus, type InlineExpansion, type MailSender, type MailSenderRegistry, type NodeRenderer, type NotificationPayload, type NotifierDriver, type NotifierRegistry, type PageMetadataAccessor, type PluginContext, type PluginCrypto, type PluginEvents, type PluginLogger, type PluginRouterScope, type RenderContext, type RenderError, type RenderPhase, type RenderResult, type RendererRegistry, type Reservation, SENSITIVE_FIELD_MARKER, type ScopedCacheStorage, type SearchDriver, type SearchHit, type SearchHits, type SearchPageType, type SearchQuery, type SearchQueryGrants, type SearchQueryViewer, type SearchRegistry, type SearchableDoc, type StorageDriver, type StoragePutMeta, type StoragePutResult, type StorageRegistry, type UrlInlineExpansionRule, getActionAnnotation, isSensitiveField };
1239
+ export { ACTION_FIELD_MARKER, type AppInfo, type AuthContext, type AuthDriver, type AuthProfile, type AuthRegistry, type AuthVerifyResult, type CacheEntry, type CacheKey, type CacheStorage, type CodeBlockInfo, type CodeBlockRenderer, type CrowiPlugin, type EmailMessage, type EmbedFragment, type EmbedInput, type EmbedRenderer, type EventBus, type InlineExpansion, type MailSender, type MailSenderRegistry, type NodeRenderer, type NotificationPayload, type NotifierDriver, type NotifierRegistry, type PageMetadataAccessor, type PluginContext, type PluginCrypto, type PluginEvents, type PluginLogger, type PluginRouteHandler, type PluginRouteMethod, type PluginRouteOptions, type PluginRouterScope, type RenderContext, type RenderError, type RenderPhase, type RenderResult, type RendererRegistry, type Reservation, SENSITIVE_FIELD_MARKER, type ScopedCacheStorage, type SearchDriver, type SearchHit, type SearchHits, type SearchPageType, type SearchQuery, type SearchQueryGrants, type SearchQueryViewer, type SearchRegistry, type SearchableDoc, type StorageDriver, type StoragePutMeta, type StoragePutResult, type StorageRegistry, type UrlInlineExpansionRule, getActionAnnotation, isSensitiveField };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { Readable } from 'node:stream';
3
+ import { Context } from 'hono';
3
4
 
4
5
  /**
5
6
  * The context object passed to every plugin callback. It is the only
@@ -29,6 +30,13 @@ interface PluginContext {
29
30
  * instead of duplicating the fields in their own configSchema.
30
31
  */
31
32
  dependencyConfig<T>(dependencyName: string): T;
33
+ /**
34
+ * Read core application info (the wiki name, …) — settings that live
35
+ * outside this plugin's own config namespace but that an integration
36
+ * may need (e.g. to brand an outbound manifest). Read live at call
37
+ * time, so it reflects admin edits made after boot.
38
+ */
39
+ appInfo(): AppInfo;
32
40
  /** Write a single config field, persisting to Mongo. */
33
41
  setConfig(key: string, value: unknown): Promise<void>;
34
42
  /** Per-Page metadata accessor for this plugin's namespace. */
@@ -48,6 +56,30 @@ interface PluginContext {
48
56
  /** Structured logger scoped to this plugin (auto-prefixed with name). */
49
57
  log: PluginLogger;
50
58
  }
59
+ /**
60
+ * Read-only view of core application settings exposed to plugins via
61
+ * `ctx.appInfo()`. Intentionally a small, curated surface (not a generic
62
+ * "read any core config" escape hatch) — add fields here as concrete
63
+ * plugin needs appear.
64
+ */
65
+ interface AppInfo {
66
+ /**
67
+ * The configured wiki name (core `app:title`), trimmed. Always a
68
+ * non-empty string: when the operator has not set a custom title it
69
+ * defaults to `'Crowi'` (the seed value), so consumers never have to
70
+ * handle an absent name.
71
+ */
72
+ title: string;
73
+ /**
74
+ * The wiki's public base origin (core `CLIENT_URL` / `getBaseUrl()`),
75
+ * e.g. `https://wiki.example.com`. An **empty string** when no public
76
+ * origin is configured — unlike `title` there is no sensible default,
77
+ * so a plugin that needs an absolute URL (outbound webhook / manifest)
78
+ * must handle the empty case. Plugins read this instead of
79
+ * `process.env.CLIENT_URL` directly.
80
+ */
81
+ baseUrl: string;
82
+ }
51
83
  /**
52
84
  * Per-Page metadata read / write helper. Each plugin gets a private
53
85
  * namespace at `page.metadata['<plugin-name>']`; this accessor scopes
@@ -910,28 +942,65 @@ interface RendererRegistry {
910
942
  }
911
943
 
912
944
  /**
913
- * Scope passed to `registerRoutes`. The HTTP route contribution surface
914
- * for plugins is **not currently wired** to the runtime — it has never
915
- * been invoked end-to-end. RFC-0006 Phase 6 removed the framework
916
- * dependency that previously typed the contract argument; the API
917
- * surface is therefore a deliberate no-op stub until a follow-up RFC
918
- * redesigns plugin HTTP contribution on top of Hono.
945
+ * HTTP method a plugin route can be mounted on. Kept to the verbs the
946
+ * inbound-webhook + admin-action surface actually needs (RFC-0013 §4):
947
+ * `POST` for Slack events / slash / interactivity + `@action` targets,
948
+ * `GET` for OAuth callbacks + simple status endpoints.
949
+ */
950
+ type PluginRouteMethod = 'GET' | 'POST';
951
+ /**
952
+ * A plugin route handler. It receives the raw Hono `Context` and returns
953
+ * a `Response` (or a promise of one), exactly like a hand-written Hono
954
+ * handler — the scope does **not** wrap it in a typed-route/validator
955
+ * layer.
919
956
  *
920
- * Plugins that declare a `registerRoutes` callback today see it
921
- * receive this scope and call `scope.register(...)` with arbitrary
922
- * arguments both arguments are accepted as `unknown` and the call
923
- * is silently dropped. The type fixture exists so the public surface
924
- * of `@crowi/plugin-api` keeps compiling against existing plugin
925
- * sources (including the in-tree `__fixtures__/example-plugin.ts`)
926
- * without forcing every plugin to be updated in lockstep with Phase 6.
957
+ * **Raw body invariant** (RFC-0013 §8, a Slack hard requirement): the
958
+ * route is a plain Hono route, NOT a `@hono/zod-openapi` route, so no
959
+ * body-consuming validator runs ahead of the handler. `c.req.text()` /
960
+ * `c.req.raw` therefore yield the *exact* bytes the client sent, which
961
+ * the Slack signature check (`HMAC-SHA256` over `v0:{ts}:{rawBody}`)
962
+ * depends on. `createJwtAuth` (installed on non-public routes) never
963
+ * reads the body, so the invariant holds for authed routes too.
964
+ */
965
+ type PluginRouteHandler = (c: Context) => Response | Promise<Response>;
966
+ /** Per-route options passed alongside the handler. */
967
+ interface PluginRouteOptions {
968
+ /**
969
+ * When `true`, the route is mounted **without** `createJwtAuth`, so it
970
+ * is reachable by unauthenticated requests (Crowi-auth public). Use for
971
+ * inbound webhooks that authenticate themselves out-of-band — e.g. the
972
+ * Slack Events API endpoint, which is gated by Slack's request-signature
973
+ * check rather than a Crowi session (RFC-0013 §8).
974
+ *
975
+ * Omitted / `false` mounts the route under `createJwtAuth`, so it
976
+ * requires a valid Crowi JWT just like a core authenticated endpoint
977
+ * (admin "Test connection" / `@action` targets, OAuth callbacks).
978
+ */
979
+ public?: boolean;
980
+ }
981
+ /**
982
+ * Scope passed to `registerRoutes(scope, ctx)`. Lets a plugin contribute
983
+ * HTTP routes that the runtime mounts at
984
+ * `/api/v2/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
985
+ * segment guarantees that core endpoints and other plugins cannot
986
+ * collide (RFC-0013 §4).
987
+ *
988
+ * The scope is built per-plugin inside `buildHonoApp` (the Hono app does
989
+ * not exist yet when plugins activate at boot), so `<plugin-name>` is
990
+ * already closed over — plugins only supply the sub-path.
927
991
  */
928
992
  interface PluginRouterScope {
929
993
  /**
930
- * No-op register. Both arguments are typed as `unknown` because the
931
- * runtime never reads them; concrete shapes are reserved for the
932
- * follow-up RFC that wires plugin HTTP contribution onto Hono.
994
+ * Mount `handler` for `method` at `<path>` under this plugin's
995
+ * namespace. `path` is relative to `/api/v2/plugins/<plugin-name>` and
996
+ * should start with `/` (e.g. `route('POST', '/events', handler, {
997
+ * public: true })` → `POST /api/v2/plugins/<name>/events`).
998
+ *
999
+ * Pass `{ public: true }` to bypass `createJwtAuth` for self-
1000
+ * authenticating inbound webhooks; omit it for routes that require a
1001
+ * Crowi session.
933
1002
  */
934
- register(contract: unknown, implementation: unknown): void;
1003
+ route(method: PluginRouteMethod, path: string, handler: PluginRouteHandler, opts?: PluginRouteOptions): void;
935
1004
  }
936
1005
 
937
1006
  /**
@@ -1002,7 +1071,7 @@ interface CrowiPlugin {
1002
1071
  * from a fixed allow-list to keep the bundle small.
1003
1072
  */
1004
1073
  adminPlacement?: {
1005
- section?: 'settings' | 'shared' | 'storage' | 'mail' | 'notification' | 'auth' | 'search' | 'renderer';
1074
+ section?: 'settings' | 'shared' | 'storage' | 'mail' | 'notification' | 'auth' | 'search' | 'renderer' | 'platform';
1006
1075
  label?: string;
1007
1076
  icon?: string;
1008
1077
  };
@@ -1052,13 +1121,24 @@ interface CrowiPlugin {
1052
1121
  */
1053
1122
  registerHooks?: (events: EventBus, ctx: PluginContext) => void;
1054
1123
  /**
1055
- * ts-rest contract that the plugin contributes. Mounted at
1056
- * `/api/v2/plugins/<name>/*` (the `<name>` path segment guarantees
1124
+ * HTTP routes the plugin contributes, mounted at
1125
+ * `/api/v2/plugins/<name>/<path>` (the `<name>` path segment guarantees
1057
1126
  * that core endpoints and other plugins cannot collide). Used for
1058
- * "Test connection" buttons, OAuth callbacks, custom admin views,
1059
- * etc. The contract surface uses ts-rest so the admin UI can call
1060
- * plugin endpoints with the same `apiClient.<plugin>.<method>` shape
1061
- * it uses for core endpoints.
1127
+ * inbound webhooks (Slack events / slash / interactivity), "Test
1128
+ * connection" buttons, `@action` targets, OAuth callbacks, etc.
1129
+ *
1130
+ * Each route is a plain Hono handler — `scope.route(method, path,
1131
+ * (c) => Response, opts?)`. The handler receives the raw `Context`, so
1132
+ * `c.req.text()` / `c.req.raw` give the exact request bytes (no
1133
+ * validator consumes the body ahead of it — the Slack signature check
1134
+ * relies on this). Pass `{ public: true }` to bypass `createJwtAuth`
1135
+ * for self-authenticating webhooks; omit it for Crowi-session-gated
1136
+ * routes.
1137
+ *
1138
+ * Called once at boot — but unlike the other `register*` hooks, this
1139
+ * runs inside `buildHonoApp` (the Hono app does not exist yet when
1140
+ * plugins activate), so a plugin's `registerRoutes` fires slightly
1141
+ * later than its `registerStorage` / `registerNotifier` / etc.
1062
1142
  */
1063
1143
  registerRoutes?: (scope: PluginRouterScope, ctx: PluginContext) => void;
1064
1144
  /**
@@ -1156,4 +1236,4 @@ interface ActionAnnotation {
1156
1236
  */
1157
1237
  declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
1158
1238
 
1159
- export { ACTION_FIELD_MARKER, type AuthContext, type AuthDriver, type AuthProfile, type AuthRegistry, type AuthVerifyResult, type CacheEntry, type CacheKey, type CacheStorage, type CodeBlockInfo, type CodeBlockRenderer, type CrowiPlugin, type EmailMessage, type EmbedFragment, type EmbedInput, type EmbedRenderer, type EventBus, type InlineExpansion, type MailSender, type MailSenderRegistry, type NodeRenderer, type NotificationPayload, type NotifierDriver, type NotifierRegistry, type PageMetadataAccessor, type PluginContext, type PluginCrypto, type PluginEvents, type PluginLogger, type PluginRouterScope, type RenderContext, type RenderError, type RenderPhase, type RenderResult, type RendererRegistry, type Reservation, SENSITIVE_FIELD_MARKER, type ScopedCacheStorage, type SearchDriver, type SearchHit, type SearchHits, type SearchPageType, type SearchQuery, type SearchQueryGrants, type SearchQueryViewer, type SearchRegistry, type SearchableDoc, type StorageDriver, type StoragePutMeta, type StoragePutResult, type StorageRegistry, type UrlInlineExpansionRule, getActionAnnotation, isSensitiveField };
1239
+ export { ACTION_FIELD_MARKER, type AppInfo, type AuthContext, type AuthDriver, type AuthProfile, type AuthRegistry, type AuthVerifyResult, type CacheEntry, type CacheKey, type CacheStorage, type CodeBlockInfo, type CodeBlockRenderer, type CrowiPlugin, type EmailMessage, type EmbedFragment, type EmbedInput, type EmbedRenderer, type EventBus, type InlineExpansion, type MailSender, type MailSenderRegistry, type NodeRenderer, type NotificationPayload, type NotifierDriver, type NotifierRegistry, type PageMetadataAccessor, type PluginContext, type PluginCrypto, type PluginEvents, type PluginLogger, type PluginRouteHandler, type PluginRouteMethod, type PluginRouteOptions, type PluginRouterScope, type RenderContext, type RenderError, type RenderPhase, type RenderResult, type RendererRegistry, type Reservation, SENSITIVE_FIELD_MARKER, type ScopedCacheStorage, type SearchDriver, type SearchHit, type SearchHits, type SearchPageType, type SearchQuery, type SearchQueryGrants, type SearchQueryViewer, type SearchRegistry, type SearchableDoc, type StorageDriver, type StoragePutMeta, type StoragePutResult, type StorageRegistry, type UrlInlineExpansionRule, getActionAnnotation, isSensitiveField };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/schema-markers.ts"],"sourcesContent":["/**\n * @crowi/plugin-api — type-only contract for Crowi 2.0 plugins.\n *\n * Plugins author against this package. The runtime (@crowi/server) loads\n * plugins listed in `crowi.config.json`, calls each plugin's\n * `register*` callbacks, and routes all the side effects (storage,\n * search, auth, notifications) through the typed registries declared\n * here.\n *\n * For the design rationale see `docs/rfcs/0001-plugin-architecture.md`\n * in the Crowi monorepo.\n */\n\nexport type { CrowiPlugin } from './plugin';\n\nexport type { PluginContext, PageMetadataAccessor, PluginCrypto, PluginLogger } from './context';\n\nexport type { StorageDriver, StorageRegistry, StoragePutMeta, StoragePutResult } from './registries/storage';\n\nexport type {\n SearchDriver,\n SearchRegistry,\n SearchableDoc,\n SearchQuery,\n SearchQueryViewer,\n SearchQueryGrants,\n SearchPageType,\n SearchHits,\n SearchHit,\n} from './registries/search';\n\nexport type { AuthDriver, AuthRegistry, AuthProfile, AuthVerifyResult } from './registries/auth';\n\nexport type { NotifierDriver, NotifierRegistry, NotificationPayload } from './registries/notifier';\n\nexport type { MailSender, MailSenderRegistry, EmailMessage } from './registries/mail';\n\nexport type {\n RendererRegistry,\n NodeRenderer,\n CodeBlockRenderer,\n CodeBlockInfo,\n EmbedRenderer,\n EmbedInput,\n EmbedFragment,\n UrlInlineExpansionRule,\n InlineExpansion,\n RenderContext,\n RenderPhase,\n RenderResult,\n RenderError,\n Reservation,\n CacheStorage,\n ScopedCacheStorage,\n CacheKey,\n CacheEntry,\n AuthContext,\n} from './renderer';\n\nexport type { EventBus, PluginEvents } from './events';\n\nexport type { PluginRouterScope } from './routes';\n\nexport { SENSITIVE_FIELD_MARKER, ACTION_FIELD_MARKER, isSensitiveField, getActionAnnotation } from './schema-markers';\n","import type { z } from 'zod/v3';\n\n/**\n * `configSchema` description-string markers.\n *\n * The admin UI walks the schema and looks at each field's\n * `description` (set via `z.string().describe('@sensitive ...')`). A\n * description starting with one of these marker tokens unlocks special\n * UI behaviour without forcing every field to declare a custom Zod\n * type.\n */\n\n/**\n * Marker that flags a config field as sensitive (encrypted at rest).\n * Usage:\n *\n * z.string().describe('@sensitive AWS secret access key')\n *\n * The runtime auto-encrypts on write and decrypts on read, using the\n * same KeyProvider as core sensitive Config. The admin UI renders the\n * field via `<SecretField>` (saved badge / clear pending / undo).\n */\nexport const SENSITIVE_FIELD_MARKER = '@sensitive';\n\n/**\n * Marker that adds an action button next to a config field. Usage:\n *\n * z.string().describe('@action \"Test connection\" POST /test')\n *\n * The admin form renders a button with the given label that calls the\n * plugin's contributed endpoint at the given verb / path (relative to\n * `/api/v2/plugins/<name>/`). Useful for \"Test connection\",\n * \"Authorise with Google\", etc. without forcing every plugin to ship\n * its own React component.\n */\nexport const ACTION_FIELD_MARKER = '@action';\n\n/**\n * True if the schema field is marked `@sensitive`.\n *\n * `field` is `z.ZodTypeAny` (intentionally loose); call sites pass the\n * value type from `configSchema.shape[key]`.\n */\nexport function isSensitiveField(field: z.ZodTypeAny): boolean {\n const description = field.description;\n return typeof description === 'string' && description.trimStart().startsWith(SENSITIVE_FIELD_MARKER);\n}\n\n/**\n * Parsed `@action` annotation extracted from a field's `description`.\n */\nexport interface ActionAnnotation {\n /** Visible button label, e.g. \"Test connection\". */\n label: string;\n /** HTTP verb of the plugin endpoint to call. */\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */\n path: string;\n}\n\n/**\n * Parse an `@action` annotation off a field, or return null if absent.\n *\n * Format: `@action \"<label>\" <METHOD> <path>`\n * e.g. `@action \"Test connection\" POST /test`\n *\n * The label may include spaces when wrapped in double quotes; the\n * method is one of `GET` / `POST` / `PUT` / `DELETE`; the path begins\n * with `/`.\n */\nexport function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null {\n const description = field.description;\n if (typeof description !== 'string') return null;\n const trimmed = description.trimStart();\n if (!trimmed.startsWith(ACTION_FIELD_MARKER)) return null;\n\n const rest = trimmed.slice(ACTION_FIELD_MARKER.length).trimStart();\n // `\"<label>\" <METHOD> <path>`\n const match = rest.match(/^\"([^\"]+)\"\\s+(GET|POST|PUT|DELETE)\\s+(\\/\\S*)/);\n if (!match) return null;\n\n const [, label, method, path] = match;\n return { label, method: method as ActionAnnotation['method'], path };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsBO,IAAM,yBAAyB;AAa/B,IAAM,sBAAsB;AAQ5B,SAAS,iBAAiB,OAA8B;AAC7D,QAAM,cAAc,MAAM;AAC1B,SAAO,OAAO,gBAAgB,YAAY,YAAY,UAAU,EAAE,WAAW,sBAAsB;AACrG;AAwBO,SAAS,oBAAoB,OAA8C;AAChF,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,SAAU,QAAO;AAC5C,QAAM,UAAU,YAAY,UAAU;AACtC,MAAI,CAAC,QAAQ,WAAW,mBAAmB,EAAG,QAAO;AAErD,QAAM,OAAO,QAAQ,MAAM,oBAAoB,MAAM,EAAE,UAAU;AAEjE,QAAM,QAAQ,KAAK,MAAM,8CAA8C;AACvE,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,EAAE,OAAO,QAAQ,IAAI,IAAI;AAChC,SAAO,EAAE,OAAO,QAA8C,KAAK;AACrE;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/schema-markers.ts"],"sourcesContent":["/**\n * @crowi/plugin-api — type-only contract for Crowi 2.0 plugins.\n *\n * Plugins author against this package. The runtime (@crowi/server) loads\n * plugins listed in `crowi.config.json`, calls each plugin's\n * `register*` callbacks, and routes all the side effects (storage,\n * search, auth, notifications) through the typed registries declared\n * here.\n *\n * For the design rationale see `docs/rfcs/0001-plugin-architecture.md`\n * in the Crowi monorepo.\n */\n\nexport type { CrowiPlugin } from './plugin';\n\nexport type { PluginContext, AppInfo, PageMetadataAccessor, PluginCrypto, PluginLogger } from './context';\n\nexport type { StorageDriver, StorageRegistry, StoragePutMeta, StoragePutResult } from './registries/storage';\n\nexport type {\n SearchDriver,\n SearchRegistry,\n SearchableDoc,\n SearchQuery,\n SearchQueryViewer,\n SearchQueryGrants,\n SearchPageType,\n SearchHits,\n SearchHit,\n} from './registries/search';\n\nexport type { AuthDriver, AuthRegistry, AuthProfile, AuthVerifyResult } from './registries/auth';\n\nexport type { NotifierDriver, NotifierRegistry, NotificationPayload } from './registries/notifier';\n\nexport type { MailSender, MailSenderRegistry, EmailMessage } from './registries/mail';\n\nexport type {\n RendererRegistry,\n NodeRenderer,\n CodeBlockRenderer,\n CodeBlockInfo,\n EmbedRenderer,\n EmbedInput,\n EmbedFragment,\n UrlInlineExpansionRule,\n InlineExpansion,\n RenderContext,\n RenderPhase,\n RenderResult,\n RenderError,\n Reservation,\n CacheStorage,\n ScopedCacheStorage,\n CacheKey,\n CacheEntry,\n AuthContext,\n} from './renderer';\n\nexport type { EventBus, PluginEvents } from './events';\n\nexport type { PluginRouterScope, PluginRouteHandler, PluginRouteMethod, PluginRouteOptions } from './routes';\n\nexport { SENSITIVE_FIELD_MARKER, ACTION_FIELD_MARKER, isSensitiveField, getActionAnnotation } from './schema-markers';\n","import type { z } from 'zod/v3';\n\n/**\n * `configSchema` description-string markers.\n *\n * The admin UI walks the schema and looks at each field's\n * `description` (set via `z.string().describe('@sensitive ...')`). A\n * description starting with one of these marker tokens unlocks special\n * UI behaviour without forcing every field to declare a custom Zod\n * type.\n */\n\n/**\n * Marker that flags a config field as sensitive (encrypted at rest).\n * Usage:\n *\n * z.string().describe('@sensitive AWS secret access key')\n *\n * The runtime auto-encrypts on write and decrypts on read, using the\n * same KeyProvider as core sensitive Config. The admin UI renders the\n * field via `<SecretField>` (saved badge / clear pending / undo).\n */\nexport const SENSITIVE_FIELD_MARKER = '@sensitive';\n\n/**\n * Marker that adds an action button next to a config field. Usage:\n *\n * z.string().describe('@action \"Test connection\" POST /test')\n *\n * The admin form renders a button with the given label that calls the\n * plugin's contributed endpoint at the given verb / path (relative to\n * `/api/v2/plugins/<name>/`). Useful for \"Test connection\",\n * \"Authorise with Google\", etc. without forcing every plugin to ship\n * its own React component.\n */\nexport const ACTION_FIELD_MARKER = '@action';\n\n/**\n * True if the schema field is marked `@sensitive`.\n *\n * `field` is `z.ZodTypeAny` (intentionally loose); call sites pass the\n * value type from `configSchema.shape[key]`.\n */\nexport function isSensitiveField(field: z.ZodTypeAny): boolean {\n const description = field.description;\n return typeof description === 'string' && description.trimStart().startsWith(SENSITIVE_FIELD_MARKER);\n}\n\n/**\n * Parsed `@action` annotation extracted from a field's `description`.\n */\nexport interface ActionAnnotation {\n /** Visible button label, e.g. \"Test connection\". */\n label: string;\n /** HTTP verb of the plugin endpoint to call. */\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */\n path: string;\n}\n\n/**\n * Parse an `@action` annotation off a field, or return null if absent.\n *\n * Format: `@action \"<label>\" <METHOD> <path>`\n * e.g. `@action \"Test connection\" POST /test`\n *\n * The label may include spaces when wrapped in double quotes; the\n * method is one of `GET` / `POST` / `PUT` / `DELETE`; the path begins\n * with `/`.\n */\nexport function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null {\n const description = field.description;\n if (typeof description !== 'string') return null;\n const trimmed = description.trimStart();\n if (!trimmed.startsWith(ACTION_FIELD_MARKER)) return null;\n\n const rest = trimmed.slice(ACTION_FIELD_MARKER.length).trimStart();\n // `\"<label>\" <METHOD> <path>`\n const match = rest.match(/^\"([^\"]+)\"\\s+(GET|POST|PUT|DELETE)\\s+(\\/\\S*)/);\n if (!match) return null;\n\n const [, label, method, path] = match;\n return { label, method: method as ActionAnnotation['method'], path };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsBO,IAAM,yBAAyB;AAa/B,IAAM,sBAAsB;AAQ5B,SAAS,iBAAiB,OAA8B;AAC7D,QAAM,cAAc,MAAM;AAC1B,SAAO,OAAO,gBAAgB,YAAY,YAAY,UAAU,EAAE,WAAW,sBAAsB;AACrG;AAwBO,SAAS,oBAAoB,OAA8C;AAChF,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,SAAU,QAAO;AAC5C,QAAM,UAAU,YAAY,UAAU;AACtC,MAAI,CAAC,QAAQ,WAAW,mBAAmB,EAAG,QAAO;AAErD,QAAM,OAAO,QAAQ,MAAM,oBAAoB,MAAM,EAAE,UAAU;AAEjE,QAAM,QAAQ,KAAK,MAAM,8CAA8C;AACvE,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,EAAE,OAAO,QAAQ,IAAI,IAAI;AAChC,SAAO,EAAE,OAAO,QAA8C,KAAK;AACrE;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowi/plugin-api",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Type-only contract for Crowi 2.0 plugins. See docs/rfcs/0001-plugin-architecture.md.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,10 +25,12 @@
25
25
  "access": "public"
26
26
  },
27
27
  "peerDependencies": {
28
+ "hono": "^4.12.25",
28
29
  "zod": "^4"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/node": "^24",
33
+ "hono": "^4.12.25",
32
34
  "tsup": "^8.3.5",
33
35
  "typescript": "^5.8.3",
34
36
  "zod": "^4.4.3",