@pithy-sh/core 0.12.0 → 0.14.0
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 +54 -0
- package/dist/audit/auditEvent.js +1 -1
- package/dist/audit/chain.d.ts +289 -0
- package/dist/audit/chain.d.ts.map +1 -0
- package/dist/audit/chain.js +400 -0
- package/dist/audit/citations.d.ts +52 -0
- package/dist/audit/citations.d.ts.map +1 -0
- package/dist/audit/citations.js +13 -0
- package/dist/audit/composed.d.ts +18 -0
- package/dist/audit/composed.d.ts.map +1 -0
- package/dist/audit/composed.js +28 -0
- package/dist/capability/bindings.d.ts +22 -0
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +32 -2
- package/dist/capability/capability.d.ts +133 -15
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/capability.js +17 -2
- package/dist/capability/erasure.d.ts +75 -0
- package/dist/capability/erasure.d.ts.map +1 -0
- package/dist/capability/erasure.js +2 -0
- package/dist/capability/manifest.d.ts +5 -0
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/personColumns.d.ts +28 -0
- package/dist/capability/personColumns.d.ts.map +1 -0
- package/dist/capability/personColumns.js +66 -0
- package/dist/capability/personRows.d.ts +62 -0
- package/dist/capability/personRows.d.ts.map +1 -0
- package/dist/capability/personRows.js +59 -0
- package/dist/capability/purge.d.ts +113 -4
- package/dist/capability/purge.d.ts.map +1 -1
- package/dist/capability/purge.js +138 -5
- package/dist/capability/settings.d.ts +10 -0
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/vectorIndex.d.ts +36 -0
- package/dist/capability/vectorIndex.d.ts.map +1 -0
- package/dist/capability/vectorIndex.js +25 -0
- package/dist/cli/commandCatalog.generated.d.ts +434 -4
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +455 -2
- package/dist/cli/commandFacts.d.ts +8 -5
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +12 -3
- package/dist/consent/purpose.d.ts +70 -0
- package/dist/consent/purpose.d.ts.map +1 -0
- package/dist/consent/purpose.js +97 -0
- package/dist/consent/reader.d.ts +99 -0
- package/dist/consent/reader.d.ts.map +1 -0
- package/dist/consent/reader.js +33 -0
- package/dist/controlPlane/audit/actions.d.ts +15 -4
- package/dist/controlPlane/audit/actions.d.ts.map +1 -1
- package/dist/controlPlane/audit/actions.js +16 -5
- package/dist/controlPlane/capability.d.ts +10 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +56 -8
- package/dist/controlPlane/context.d.ts +4 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -2
- package/dist/controlPlane/data/connection.d.ts +21 -2
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +13 -4
- package/dist/controlPlane/data/connections.d.ts +94 -0
- package/dist/controlPlane/data/connections.d.ts.map +1 -0
- package/dist/controlPlane/data/connections.js +151 -0
- package/dist/controlPlane/data/hold.d.ts +46 -0
- package/dist/controlPlane/data/hold.d.ts.map +1 -0
- package/dist/controlPlane/data/hold.js +36 -0
- package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
- package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
- package/dist/controlPlane/data/oidcTrust.js +173 -0
- package/dist/controlPlane/data/tables.d.ts +4 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -1
- package/dist/controlPlane/data/tables.js +9 -4
- package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
- package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
- package/dist/controlPlane/data/trustFirstCall.js +25 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
- package/dist/controlPlane/discovery/adminRoute.js +32 -8
- package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
- package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
- package/dist/controlPlane/discovery/routeInput.js +80 -0
- package/dist/controlPlane/discovery/toolName.d.ts +21 -0
- package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
- package/dist/controlPlane/discovery/toolName.js +25 -0
- package/dist/controlPlane/erasure/attest.d.ts +78 -0
- package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
- package/dist/controlPlane/erasure/attest.js +164 -0
- package/dist/controlPlane/erasure/read.d.ts +39 -0
- package/dist/controlPlane/erasure/read.d.ts.map +1 -0
- package/dist/controlPlane/erasure/read.js +140 -0
- package/dist/controlPlane/error/errors.d.ts +61 -2
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +13 -0
- package/dist/controlPlane/evidence/derive.d.ts +124 -0
- package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
- package/dist/controlPlane/evidence/derive.js +343 -0
- package/dist/controlPlane/evidence/mapping.d.ts +85 -0
- package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
- package/dist/controlPlane/evidence/mapping.js +114 -0
- package/dist/controlPlane/evidence/pack.d.ts +46 -0
- package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
- package/dist/controlPlane/evidence/pack.js +124 -0
- package/dist/controlPlane/http/connections.d.ts +96 -0
- package/dist/controlPlane/http/connections.d.ts.map +1 -0
- package/dist/controlPlane/http/connections.js +109 -0
- package/dist/controlPlane/http/guard.d.ts +23 -2
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +103 -7
- package/dist/controlPlane/http/handlers.d.ts +212 -10
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +117 -26
- package/dist/controlPlane/http/holds.d.ts +57 -0
- package/dist/controlPlane/http/holds.d.ts.map +1 -0
- package/dist/controlPlane/http/holds.js +98 -0
- package/dist/controlPlane/http/presence.d.ts +27 -0
- package/dist/controlPlane/http/presence.d.ts.map +1 -0
- package/dist/controlPlane/http/presence.js +67 -0
- package/dist/controlPlane/http/processing.d.ts +73 -0
- package/dist/controlPlane/http/processing.d.ts.map +1 -0
- package/dist/controlPlane/http/processing.js +17 -0
- package/dist/controlPlane/http/responses.d.ts +927 -3
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +283 -7
- package/dist/controlPlane/http/routes.d.ts +12 -2
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +182 -5
- package/dist/controlPlane/http/schemas.d.ts +63 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -1
- package/dist/controlPlane/http/schemas.js +39 -1
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +13 -1
- package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
- package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
- package/dist/controlPlane/http/verifyTrust.js +75 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0005_holds.js +26 -0
- package/dist/controlPlane/processing/derive.d.ts +77 -0
- package/dist/controlPlane/processing/derive.d.ts.map +1 -0
- package/dist/controlPlane/processing/derive.js +206 -0
- package/dist/controlPlane/processing/read.d.ts +10 -0
- package/dist/controlPlane/processing/read.d.ts.map +1 -0
- package/dist/controlPlane/processing/read.js +89 -0
- package/dist/controlPlane/scope/kind.d.ts +46 -0
- package/dist/controlPlane/scope/kind.d.ts.map +1 -0
- package/dist/controlPlane/scope/kind.js +52 -0
- package/dist/controlPlane/scope/scope.d.ts +58 -3
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +59 -4
- package/dist/controlPlane/token/jws.js +1 -1
- package/dist/controlPlane/token/mint.js +1 -1
- package/dist/controlPlane/wire.d.ts +23 -0
- package/dist/controlPlane/wire.d.ts.map +1 -1
- package/dist/controlPlane/wire.js +24 -1
- package/dist/createBackend.d.ts +13 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +11 -3
- package/dist/createEntrypoint.d.ts +14 -1
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +19 -5
- package/dist/error/http.d.ts +216 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +432 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +144 -0
- package/dist/error/pithyError.d.ts +22 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +31 -1
- package/dist/http/routeSpec.d.ts +46 -1
- package/dist/http/routeSpec.d.ts.map +1 -1
- package/dist/http/routeSpec.js +39 -5
- package/dist/http/validation.d.ts +11 -2
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +20 -3
- package/dist/http/verification.d.ts.map +1 -1
- package/dist/http/verification.js +1 -1
- package/dist/i18n/translator.d.ts +2 -1
- package/dist/i18n/translator.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/logger/record.d.ts +126 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/mcp/annotations.d.ts +20 -0
- package/dist/mcp/annotations.d.ts.map +1 -0
- package/dist/mcp/annotations.js +23 -0
- package/dist/mcp/tools.d.ts +75 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +118 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +8 -26
- package/dist/openapi/operations.d.ts +35 -0
- package/dist/openapi/operations.d.ts.map +1 -0
- package/dist/openapi/operations.js +37 -0
- package/dist/presence/channel.d.ts +11 -0
- package/dist/presence/channel.d.ts.map +1 -0
- package/dist/presence/channel.js +65 -0
- package/dist/presence/declaration.d.ts +154 -0
- package/dist/presence/declaration.d.ts.map +1 -0
- package/dist/presence/declaration.js +164 -0
- package/dist/presence/durableObject.d.ts +61 -0
- package/dist/presence/durableObject.d.ts.map +1 -0
- package/dist/presence/durableObject.js +257 -0
- package/dist/presence/frames.d.ts +166 -0
- package/dist/presence/frames.d.ts.map +1 -0
- package/dist/presence/frames.js +90 -0
- package/dist/presence/http/guard.d.ts +11 -0
- package/dist/presence/http/guard.d.ts.map +1 -0
- package/dist/presence/http/guard.js +20 -0
- package/dist/presence/http/responses.d.ts +31 -0
- package/dist/presence/http/responses.d.ts.map +1 -0
- package/dist/presence/http/responses.js +34 -0
- package/dist/presence/http/routes.d.ts +24 -0
- package/dist/presence/http/routes.d.ts.map +1 -0
- package/dist/presence/http/routes.js +127 -0
- package/dist/presence/http/schemas.d.ts +29 -0
- package/dist/presence/http/schemas.d.ts.map +1 -0
- package/dist/presence/http/schemas.js +28 -0
- package/dist/presence/names.d.ts +23 -0
- package/dist/presence/names.d.ts.map +1 -0
- package/dist/presence/names.js +22 -0
- package/dist/presence/room.d.ts +95 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +105 -0
- package/dist/presence/ticket.d.ts +55 -0
- package/dist/presence/ticket.d.ts.map +1 -0
- package/dist/presence/ticket.js +109 -0
- package/dist/privacy/classify.js +1 -1
- package/dist/privacy/encrypted.js +1 -1
- package/dist/processing/activity.d.ts +134 -0
- package/dist/processing/activity.d.ts.map +1 -0
- package/dist/processing/activity.js +161 -0
- package/dist/processing/composed.d.ts +18 -0
- package/dist/processing/composed.d.ts.map +1 -0
- package/dist/processing/composed.js +28 -0
- package/dist/processing/vocabulary.d.ts +26 -0
- package/dist/processing/vocabulary.d.ts.map +1 -0
- package/dist/processing/vocabulary.js +35 -0
- package/dist/queue/consumer.d.ts +59 -0
- package/dist/queue/consumer.d.ts.map +1 -0
- package/dist/queue/consumer.js +46 -0
- package/dist/retention/composed.d.ts +37 -0
- package/dist/retention/composed.d.ts.map +1 -0
- package/dist/retention/composed.js +66 -0
- package/dist/retention/holds.d.ts +61 -0
- package/dist/retention/holds.d.ts.map +1 -0
- package/dist/retention/holds.js +107 -0
- package/dist/retention/holdsPurge.d.ts +22 -0
- package/dist/retention/holdsPurge.d.ts.map +1 -0
- package/dist/retention/holdsPurge.js +50 -0
- package/dist/retention/policy.d.ts +132 -0
- package/dist/retention/policy.d.ts.map +1 -0
- package/dist/retention/policy.js +197 -0
- package/dist/session/rotated.d.ts +34 -0
- package/dist/session/rotated.d.ts.map +1 -0
- package/dist/session/rotated.js +25 -0
- package/dist/test-utils/connectionFixtures.d.ts +13 -0
- package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
- package/dist/test-utils/connectionFixtures.js +58 -0
- package/dist/test-utils/interleave.d.ts +12 -0
- package/dist/test-utils/interleave.d.ts.map +1 -0
- package/dist/test-utils/interleave.js +26 -0
- package/dist/testWorker.d.ts +8 -0
- package/dist/testWorker.d.ts.map +1 -0
- package/dist/testWorker.js +9 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflow/dispatchRoute.js +1 -1
- package/package.json +1 -1
- package/src/audit/auditEvent.ts +1 -1
- package/src/audit/chain.ts +551 -0
- package/src/audit/citations.ts +63 -0
- package/src/audit/composed.ts +30 -0
- package/src/capability/bindings.ts +61 -0
- package/src/capability/capability.ts +166 -18
- package/src/capability/erasure.ts +82 -0
- package/src/capability/personColumns.ts +83 -0
- package/src/capability/personRows.ts +125 -0
- package/src/capability/purge.ts +208 -8
- package/src/capability/settings.ts +7 -0
- package/src/capability/vectorIndex.ts +60 -0
- package/src/cli/commandCatalog.generated.ts +488 -2
- package/src/cli/commandFacts.ts +12 -3
- package/src/cloudflare-test.d.ts +3 -1
- package/src/consent/purpose.ts +160 -0
- package/src/consent/reader.ts +134 -0
- package/src/controlPlane/audit/actions.ts +15 -4
- package/src/controlPlane/capability.ts +99 -20
- package/src/controlPlane/context.ts +10 -2
- package/src/controlPlane/data/connection.ts +16 -2
- package/src/controlPlane/data/connections.ts +231 -0
- package/src/controlPlane/data/hold.ts +52 -0
- package/src/controlPlane/data/oidcTrust.ts +249 -0
- package/src/controlPlane/data/tables.ts +7 -0
- package/src/controlPlane/data/trustFirstCall.ts +37 -0
- package/src/controlPlane/discovery/adminRoute.ts +71 -9
- package/src/controlPlane/discovery/routeInput.ts +86 -0
- package/src/controlPlane/discovery/toolName.ts +23 -0
- package/src/controlPlane/erasure/attest.ts +205 -0
- package/src/controlPlane/erasure/read.ts +169 -0
- package/src/controlPlane/error/errors.ts +69 -2
- package/src/controlPlane/evidence/derive.ts +426 -0
- package/src/controlPlane/evidence/mapping.ts +118 -0
- package/src/controlPlane/evidence/pack.ts +190 -0
- package/src/controlPlane/http/connections.ts +129 -0
- package/src/controlPlane/http/guard.ts +178 -10
- package/src/controlPlane/http/handlers.ts +172 -40
- package/src/controlPlane/http/holds.ts +108 -0
- package/src/controlPlane/http/presence.ts +71 -0
- package/src/controlPlane/http/processing.ts +19 -0
- package/src/controlPlane/http/responses.ts +590 -7
- package/src/controlPlane/http/routes.ts +302 -3
- package/src/controlPlane/http/schemas.ts +126 -0
- package/src/controlPlane/http/verify.ts +19 -2
- package/src/controlPlane/http/verifyTrust.ts +176 -0
- package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
- package/src/controlPlane/migrations/0005_holds.ts +40 -0
- package/src/controlPlane/processing/derive.ts +359 -0
- package/src/controlPlane/processing/read.ts +102 -0
- package/src/controlPlane/scope/kind.ts +63 -0
- package/src/controlPlane/scope/scope.ts +65 -3
- package/src/controlPlane/wire.ts +26 -0
- package/src/createBackend.ts +31 -6
- package/src/createEntrypoint.ts +34 -2
- package/src/error/payload.ts +302 -0
- package/src/error/pithyError.ts +42 -0
- package/src/http/routeSpec.ts +75 -5
- package/src/http/validation.ts +35 -3
- package/src/http/verification.ts +3 -1
- package/src/i18n/translator.ts +2 -1
- package/src/index.ts +2 -0
- package/src/mcp/annotations.ts +38 -0
- package/src/mcp/tools.ts +220 -0
- package/src/openapi/document.ts +8 -28
- package/src/openapi/operations.ts +62 -0
- package/src/presence/channel.ts +46 -0
- package/src/presence/declaration.ts +283 -0
- package/src/presence/durableObject.ts +287 -0
- package/src/presence/frames.ts +153 -0
- package/src/presence/http/guard.ts +23 -0
- package/src/presence/http/responses.ts +63 -0
- package/src/presence/http/routes.ts +165 -0
- package/src/presence/http/schemas.ts +39 -0
- package/src/presence/names.ts +45 -0
- package/src/presence/room.ts +145 -0
- package/src/presence/ticket.ts +145 -0
- package/src/processing/activity.ts +289 -0
- package/src/processing/composed.ts +33 -0
- package/src/processing/vocabulary.ts +43 -0
- package/src/queue/consumer.ts +104 -0
- package/src/retention/composed.ts +75 -0
- package/src/retention/holds.ts +175 -0
- package/src/retention/holdsPurge.ts +50 -0
- package/src/retention/policy.ts +305 -0
- package/src/session/rotated.ts +53 -0
- package/src/test-utils/connectionFixtures.ts +58 -0
- package/src/test-utils/interleave.ts +31 -0
- package/src/testWorker.ts +13 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { ScopeKind } from "../controlPlane/scope/kind";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What an MCP tool says about itself, from the kind of thing it does. One mapping for both servers: the app's own
|
|
8
|
+
* tools, whose effect is a kind without `reads` or `credentials`, and the control-plane tools `pithy mcp serve`
|
|
9
|
+
* offers, whose route declares its scope's kind.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The MCP annotations a tool carries, set by its kind. `idempotentHint` is claimed for a read and nothing else. */
|
|
13
|
+
export interface ToolAnnotations {
|
|
14
|
+
readonly readOnlyHint: boolean;
|
|
15
|
+
readonly destructiveHint: boolean;
|
|
16
|
+
readonly idempotentHint?: true;
|
|
17
|
+
readonly openWorldHint: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What each kind says in a tool's description, and on the consent screen. One sentence each. */
|
|
21
|
+
export const KIND_SENTENCES: Readonly<Record<ScopeKind, string>> = {
|
|
22
|
+
reads: "Reads data and changes nothing.",
|
|
23
|
+
writes: "Changes data in this application.",
|
|
24
|
+
sends: "Sends a message to somebody.",
|
|
25
|
+
charges: "Charges money.",
|
|
26
|
+
deletes: "Deletes data, and it cannot be undone.",
|
|
27
|
+
credentials: "Mints, rotates or revokes a credential.",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The MCP annotations for a kind. */
|
|
31
|
+
export function kindAnnotations(kind: ScopeKind): ToolAnnotations {
|
|
32
|
+
return {
|
|
33
|
+
readOnlyHint: kind === "reads",
|
|
34
|
+
destructiveHint: kind === "charges" || kind === "deletes" || kind === "credentials",
|
|
35
|
+
...(kind === "reads" ? { idempotentHint: true as const } : {}),
|
|
36
|
+
openWorldHint: kind === "sends" || kind === "charges",
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { AdminOperation } from "../controlPlane/discovery/adminRoute";
|
|
5
|
+
import { inputOf } from "../controlPlane/discovery/routeInput";
|
|
6
|
+
import { controlPlaneToolName } from "../controlPlane/discovery/toolName";
|
|
7
|
+
import { ValidationError } from "../error/pithyError";
|
|
8
|
+
import { type RouteSpec, routeSpecOf, type ToolEffect } from "../http/routeSpec";
|
|
9
|
+
import type { RouteGroup } from "../openapi/document";
|
|
10
|
+
import { isMiddlewareOnly, operationsOf } from "../openapi/operations";
|
|
11
|
+
import { KIND_SENTENCES, kindAnnotations, type ToolAnnotations } from "./annotations";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The app's own MCP tools, read off the composed router (`docs/AGENT-ACCESS.md` §4.1, §5).
|
|
15
|
+
*
|
|
16
|
+
* **The route declarations are already a tool list.** A route that opts in with `describeRoute({ tool })` carries
|
|
17
|
+
* everything a tool definition needs: the `summary` an agent chooses on, a `description` for what the schemas do not
|
|
18
|
+
* say, the `power` that gates it, the `scopes` a caller must carry, and an `operation` that names it. Its input is
|
|
19
|
+
* the route's own `validate(...)` schemas, in the same `{ path, query, body }` shape the control-plane server's tools
|
|
20
|
+
* take. Nothing is written twice: a route that changes its body, its power or its summary changes its tool on the
|
|
21
|
+
* same commit.
|
|
22
|
+
*
|
|
23
|
+
* **A tool inherits its route's power. That is the whole mapping**, and there is no tool-to-role table to maintain.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** One route, as a tool. */
|
|
27
|
+
export interface AppTool {
|
|
28
|
+
/** `<capability>_<operation>`, through the control-plane server's normalizer. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** The capability that registered the route — the unit of consent. */
|
|
31
|
+
readonly capability: string;
|
|
32
|
+
/** The route's declared operation. */
|
|
33
|
+
readonly operation: string;
|
|
34
|
+
/** The HTTP method a call dispatches with. */
|
|
35
|
+
readonly method: string;
|
|
36
|
+
/** The mounted path a call dispatches to, Hono's spelling. */
|
|
37
|
+
readonly path: string;
|
|
38
|
+
/** The route's summary, in its capability's own words. What the consent screen lists. */
|
|
39
|
+
readonly summary: string;
|
|
40
|
+
/** What an agent reads: the summary, the route's description, and its effect said aloud. */
|
|
41
|
+
readonly description: string;
|
|
42
|
+
/** The route's input as JSON Schema. */
|
|
43
|
+
readonly inputSchema: Record<string, unknown>;
|
|
44
|
+
/** What the tool does beyond reading, or `null` for a read. */
|
|
45
|
+
readonly effect: ToolEffect | null;
|
|
46
|
+
/** The MCP annotations its effect sets. */
|
|
47
|
+
readonly annotations: ToolAnnotations;
|
|
48
|
+
/** The power the caller's role must hold, or `undefined` when the route declares none. */
|
|
49
|
+
readonly power: string | undefined;
|
|
50
|
+
/** The attestation scopes a caller must carry, from `describeRoute({ scopes })`. */
|
|
51
|
+
readonly scopes: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What each effect says in a tool's description and on the consent screen. One sentence each. */
|
|
55
|
+
export const EFFECT_SENTENCES: Readonly<Record<ToolEffect, string>> = {
|
|
56
|
+
writes: KIND_SENTENCES.writes,
|
|
57
|
+
sends: KIND_SENTENCES.sends,
|
|
58
|
+
charges: KIND_SENTENCES.charges,
|
|
59
|
+
deletes: KIND_SENTENCES.deletes,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The MCP annotations for an effect. `null` is a read. */
|
|
63
|
+
export function toolAnnotations(effect: ToolEffect | null): ToolAnnotations {
|
|
64
|
+
return kindAnnotations(effect ?? "reads");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One described route that opted in, before it is a tool. */
|
|
68
|
+
interface Declared {
|
|
69
|
+
readonly capability: string;
|
|
70
|
+
readonly method: string;
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly spec: RouteSpec;
|
|
73
|
+
readonly handlers: readonly unknown[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Every described route in these groups, with the method it is mounted under. */
|
|
77
|
+
function described(groups: readonly RouteGroup[]): Declared[] {
|
|
78
|
+
return groups.flatMap((group) =>
|
|
79
|
+
operationsOf(group.routes)
|
|
80
|
+
.filter((operation) => !isMiddlewareOnly(operation))
|
|
81
|
+
.flatMap((operation) => {
|
|
82
|
+
const spec = operation.handlers.map(routeSpecOf).find((candidate) => candidate !== undefined);
|
|
83
|
+
return spec === undefined
|
|
84
|
+
? []
|
|
85
|
+
: [
|
|
86
|
+
{
|
|
87
|
+
capability: group.tag,
|
|
88
|
+
method: operation.method.toUpperCase(),
|
|
89
|
+
path: operation.path,
|
|
90
|
+
spec,
|
|
91
|
+
handlers: operation.handlers,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function refuse(message: string, detail: string): never {
|
|
99
|
+
throw new ValidationError({
|
|
100
|
+
message,
|
|
101
|
+
action:
|
|
102
|
+
"Fix the route's describeRoute({ operation, tool }). A GET opts in with `tool: true`; any other method declares `tool: { effect }`.",
|
|
103
|
+
detail,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Refuse a composition whose tool declarations are wrong, naming the route.
|
|
109
|
+
*
|
|
110
|
+
* **This is where "it does not compile" is enforced.** `describeRoute` cannot see the method it is mounted under, so
|
|
111
|
+
* a `POST` saying `tool: true` type-checks; `createBackend` calls this at assembly, so the Worker refuses to boot
|
|
112
|
+
* and the kit's own composition fails its tests. Five checks: a non-`GET` opting in without an effect, a `GET`
|
|
113
|
+
* declaring one, a tool with no operation, an operation outside the published alphabet, and two routes sharing an
|
|
114
|
+
* operation within a capability — or two capabilities whose names normalize to one tool.
|
|
115
|
+
*/
|
|
116
|
+
export function assertToolDeclarations(groups: readonly RouteGroup[]): void {
|
|
117
|
+
const operations = new Map<string, string>();
|
|
118
|
+
const names = new Map<string, string>();
|
|
119
|
+
for (const route of described(groups)) {
|
|
120
|
+
const where = `${route.method} ${route.path}`;
|
|
121
|
+
const { operation, tool } = route.spec;
|
|
122
|
+
if (operation !== undefined) {
|
|
123
|
+
if (!AdminOperation.safeParse(operation).success) {
|
|
124
|
+
refuse(
|
|
125
|
+
`The operation "${operation}" on ${where} is not a valid name.`,
|
|
126
|
+
`capability ${route.capability} route ${where} declares operation "${operation}"; an operation is lowercase words joined by ".", "_" or "-"`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const key = `${route.capability} ${operation}`;
|
|
130
|
+
const earlier = operations.get(key);
|
|
131
|
+
if (earlier !== undefined) {
|
|
132
|
+
refuse(
|
|
133
|
+
`Two ${route.capability} routes declare the operation "${operation}".`,
|
|
134
|
+
`${earlier} and ${where} both declare operation "${operation}" in capability ${route.capability}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
operations.set(key, where);
|
|
138
|
+
}
|
|
139
|
+
if (tool === undefined) continue;
|
|
140
|
+
if (operation === undefined) {
|
|
141
|
+
refuse(
|
|
142
|
+
`${where} opts in as a tool with no operation.`,
|
|
143
|
+
`capability ${route.capability} route ${where} declares tool but no operation, so its tool has no name`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (route.method !== "GET" && tool === true) {
|
|
147
|
+
refuse(
|
|
148
|
+
`${where} opts in as a tool without an effect.`,
|
|
149
|
+
`capability ${route.capability} route ${where} is not a GET, so it must declare tool: { effect } (writes, sends, charges or deletes)`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (route.method === "GET" && tool !== true) {
|
|
153
|
+
refuse(
|
|
154
|
+
`${where} is a read and declares an effect.`,
|
|
155
|
+
`capability ${route.capability} route ${where} is a GET; a GET opts in with tool: true`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const name = controlPlaneToolName(route.capability, operation);
|
|
159
|
+
const clash = names.get(name);
|
|
160
|
+
if (clash !== undefined) {
|
|
161
|
+
refuse(`Two routes are both the tool ${name}.`, `${clash} and ${where} both normalize to the tool name ${name}`);
|
|
162
|
+
}
|
|
163
|
+
names.set(name, where);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The description an agent reads: the summary, the route's own description, and its effect. */
|
|
168
|
+
function descriptionOf(spec: RouteSpec, effect: ToolEffect | null): string {
|
|
169
|
+
return [spec.summary, spec.description, effect === null ? undefined : EFFECT_SENTENCES[effect]]
|
|
170
|
+
.filter((part): part is string => part !== undefined && part.length > 0)
|
|
171
|
+
.join(" ");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Every tool these groups declare, in registration order. Refuses a wrong declaration first. */
|
|
175
|
+
export function appTools(groups: readonly RouteGroup[]): AppTool[] {
|
|
176
|
+
assertToolDeclarations(groups);
|
|
177
|
+
return described(groups).flatMap((route): AppTool[] => {
|
|
178
|
+
const { spec } = route;
|
|
179
|
+
if (spec.tool === undefined || spec.operation === undefined) return [];
|
|
180
|
+
const effect = spec.tool === true ? null : spec.tool.effect;
|
|
181
|
+
return [
|
|
182
|
+
{
|
|
183
|
+
name: controlPlaneToolName(route.capability, spec.operation),
|
|
184
|
+
capability: route.capability,
|
|
185
|
+
operation: spec.operation,
|
|
186
|
+
method: route.method,
|
|
187
|
+
path: route.path,
|
|
188
|
+
summary: spec.summary,
|
|
189
|
+
description: descriptionOf(spec, effect),
|
|
190
|
+
inputSchema: inputOf(route.handlers),
|
|
191
|
+
effect,
|
|
192
|
+
annotations: toolAnnotations(effect),
|
|
193
|
+
power: spec.power,
|
|
194
|
+
scopes: spec.scopes ?? [],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** What a caller holds, for {@link offeredTools}. */
|
|
201
|
+
export interface ToolCaller {
|
|
202
|
+
/** Whether the caller's role holds a power, or `null` when no organization is in force — nothing is held. */
|
|
203
|
+
readonly roleAllows: ((power: string) => boolean) | null;
|
|
204
|
+
/** The attestation scopes the caller carries. */
|
|
205
|
+
readonly scopes: readonly string[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Whether this caller may call this tool, by exactly the checks its route makes. */
|
|
209
|
+
export function mayCall(tool: AppTool, caller: ToolCaller): boolean {
|
|
210
|
+
if (tool.power !== undefined && (caller.roleAllows === null || !caller.roleAllows(tool.power))) return false;
|
|
211
|
+
return tool.scopes.every((scope) => caller.scopes.includes(scope));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The tools this caller may call. **A tool a role withholds is absent, not locked**: offering it discloses that it
|
|
216
|
+
* exists and spends a call to learn what the answer would be (`docs/AGENT-ACCESS.md` §4.1).
|
|
217
|
+
*/
|
|
218
|
+
export function offeredTools(tools: readonly AppTool[], caller: ToolCaller): AppTool[] {
|
|
219
|
+
return tools.filter((tool) => mayCall(tool, caller));
|
|
220
|
+
}
|
package/src/openapi/document.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { pathParams } from "../http/routeContract";
|
|
|
9
9
|
import { type DocumentedMethod, type RouteResponse, type RouteSpec, routeSpecOf } from "../http/routeSpec";
|
|
10
10
|
import { type RequestContract, requestContractOf } from "../http/validation";
|
|
11
11
|
import type { VerificationStrategy } from "../http/verification";
|
|
12
|
+
import { isMiddlewareOnly, type Operation, operationsOf } from "./operations";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* **The OpenAPI contract, read off a composed app.**
|
|
@@ -76,7 +77,8 @@ const SECURITY_SCHEMES: Record<string, unknown> = {
|
|
|
76
77
|
bearer: {
|
|
77
78
|
type: "http",
|
|
78
79
|
scheme: "bearer",
|
|
79
|
-
description:
|
|
80
|
+
description:
|
|
81
|
+
"A session token from `@pithy-sh/auth`, sent as `Authorization: Bearer <token>`: the one sign-in returns, then the `refreshToken` each `/token/rotate` answers with. The rotation's `accessToken` is a JWT for services outside the Worker, and these routes refuse it.",
|
|
80
82
|
},
|
|
81
83
|
session: {
|
|
82
84
|
type: "apiKey",
|
|
@@ -134,33 +136,6 @@ export function jsonSchema(schema: z.ZodType, io: "input" | "output"): JsonSchem
|
|
|
134
136
|
return rest;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
/** One route's handlers, grouped by method and path the way Hono stores them: one entry per handler. */
|
|
138
|
-
interface Operation {
|
|
139
|
-
readonly method: string;
|
|
140
|
-
readonly path: string;
|
|
141
|
-
readonly handlers: unknown[];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function operationsOf(routes: readonly RouterRoute[]): Operation[] {
|
|
145
|
-
const byKey = new Map<string, Operation>();
|
|
146
|
-
for (const route of routes) {
|
|
147
|
-
const key = `${route.method} ${route.path}`;
|
|
148
|
-
const entry = byKey.get(key) ?? { method: route.method, path: route.path, handlers: [] };
|
|
149
|
-
entry.handlers.push(route.handler);
|
|
150
|
-
byKey.set(key, entry);
|
|
151
|
-
}
|
|
152
|
-
return [...byKey.values()];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* An `ALL` entry with no spec is `app.use` — middleware, not a route — because Hono records both as
|
|
157
|
-
* `ALL`. An `app.all` route that forgot its `describeRoute` is therefore invisible to the coverage
|
|
158
|
-
* check, which is the one blind spot here; the kit has one such route and it is described.
|
|
159
|
-
*/
|
|
160
|
-
function isMiddlewareOnly(operation: Operation): boolean {
|
|
161
|
-
return operation.method === "ALL" && !operation.handlers.some((handler) => routeSpecOf(handler));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
139
|
/** Every route in these groups that registered no `describeRoute`. Empty is the contract being met. */
|
|
165
140
|
export function undescribedRoutes(groups: readonly RouteGroup[]): UndescribedRoute[] {
|
|
166
141
|
return groups.flatMap((group) =>
|
|
@@ -274,6 +249,11 @@ function operationOf(tag: string, method: string, operation: Operation, spec: Ro
|
|
|
274
249
|
responses,
|
|
275
250
|
...(strategies.length === 0 ? {} : { security, "x-pithy-verification": strategies }),
|
|
276
251
|
...(spec.scopes === undefined || spec.scopes.length === 0 ? {} : { "x-pithy-scopes": spec.scopes }),
|
|
252
|
+
// What the route declared for the app's MCP endpoint: the power its gate installs, its permanent name,
|
|
253
|
+
// and its effect when it is a tool. A client generated from the contract reads the gate it will meet.
|
|
254
|
+
...(spec.power === undefined ? {} : { "x-pithy-power": spec.power }),
|
|
255
|
+
...(spec.operation === undefined ? {} : { "x-pithy-operation": spec.operation }),
|
|
256
|
+
...(spec.tool === undefined ? {} : { "x-pithy-tool": spec.tool === true ? { effect: null } : spec.tool }),
|
|
277
257
|
};
|
|
278
258
|
}
|
|
279
259
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { type RouteSpec, routeSpecOf } from "../http/routeSpec";
|
|
5
|
+
import type { RouteGroup, RouterRoute } from "./document";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The composed route table as operations: one per method and path, with the spec its `describeRoute` declared.
|
|
9
|
+
*
|
|
10
|
+
* Hono keeps one entry per handler, so a route with a `describeRoute` ahead of its handler appears twice in
|
|
11
|
+
* `app.routes`. Two readers need the grouped view: the OpenAPI document, which turns each operation into a path
|
|
12
|
+
* item, and the evidence pack (#821), which counts how every route verifies its caller. They group the same way
|
|
13
|
+
* because they group here, and the pack never loads the JSON Schema conversion the document needs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One route's handlers, grouped by method and path the way Hono stores them: one entry per handler. */
|
|
17
|
+
export interface Operation {
|
|
18
|
+
readonly method: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly handlers: unknown[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Group a route table into operations, in registration order. */
|
|
24
|
+
export function operationsOf(routes: readonly RouterRoute[]): Operation[] {
|
|
25
|
+
const byKey = new Map<string, Operation>();
|
|
26
|
+
for (const route of routes) {
|
|
27
|
+
const key = `${route.method} ${route.path}`;
|
|
28
|
+
const entry = byKey.get(key) ?? { method: route.method, path: route.path, handlers: [] };
|
|
29
|
+
entry.handlers.push(route.handler);
|
|
30
|
+
byKey.set(key, entry);
|
|
31
|
+
}
|
|
32
|
+
return [...byKey.values()];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An `ALL` entry with no spec is `app.use` — middleware, not a route — because Hono records both as
|
|
37
|
+
* `ALL`. An `app.all` route that forgot its `describeRoute` is therefore invisible to the coverage
|
|
38
|
+
* check, which is the one blind spot here; the kit has one such route and it is described.
|
|
39
|
+
*/
|
|
40
|
+
export function isMiddlewareOnly(operation: Operation): boolean {
|
|
41
|
+
return operation.method === "ALL" && !operation.handlers.some((handler) => routeSpecOf(handler));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One route, and the spec it declared — `undefined` for a route that registered no `describeRoute`. */
|
|
45
|
+
export interface DescribedOperation {
|
|
46
|
+
readonly method: string;
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly spec: RouteSpec | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Every route in these groups, middleware left out, each with the spec it declared or `undefined`. */
|
|
52
|
+
export function describedOperations(groups: readonly RouteGroup[]): DescribedOperation[] {
|
|
53
|
+
return groups.flatMap((group) =>
|
|
54
|
+
operationsOf(group.routes)
|
|
55
|
+
.filter((operation) => !isMiddlewareOnly(operation))
|
|
56
|
+
.map((operation) => ({
|
|
57
|
+
method: operation.method,
|
|
58
|
+
path: operation.path,
|
|
59
|
+
spec: operation.handlers.map(routeSpecOf).find((candidate) => candidate !== undefined),
|
|
60
|
+
})),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { SocketChannel } from "../http/socket";
|
|
5
|
+
import { type PresenceDeclaration, presenceSocketPath } from "./declaration";
|
|
6
|
+
import {
|
|
7
|
+
PRESENCE_ROOM_VERSIONS,
|
|
8
|
+
PresenceActivityFrame,
|
|
9
|
+
PresenceJoinedFrame,
|
|
10
|
+
PresenceLeftFrame,
|
|
11
|
+
PresenceRoomErrorFrame,
|
|
12
|
+
PresenceRoomInitFrame,
|
|
13
|
+
PresenceRoomPing,
|
|
14
|
+
PresenceRoomPong,
|
|
15
|
+
PresenceSetActivityFrame,
|
|
16
|
+
} from "./frames";
|
|
17
|
+
import { PresenceRoomParams } from "./http/schemas";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A capability's presence room socket, declared for the AsyncAPI contract. `defineCapability` adds it to the
|
|
21
|
+
* capability's `sockets` whenever it declares room kinds, from the same `basePath` the routes mount on, so a
|
|
22
|
+
* declared socket and a mounted one cannot be two different things.
|
|
23
|
+
*/
|
|
24
|
+
export function presenceRoomsSocket(capability: string, presence: PresenceDeclaration): SocketChannel {
|
|
25
|
+
return {
|
|
26
|
+
name: `${capability}PresenceRooms`,
|
|
27
|
+
path: presenceSocketPath(presence.basePath),
|
|
28
|
+
summary: `Who else has a ${capability} record open, and what they are doing, live.`,
|
|
29
|
+
description: `Mint a ticket first — \`POST /control-plane/presence/tickets\` for a management client, \`POST ${presence.basePath}/presence/tickets\` for a signed-in user — then offer \`pithy.presence.v1\` and \`pithy.ticket.<ticket>\` in \`Sec-WebSocket-Protocol\`. The ticket is the credential: single use, 60 seconds, this room only. The room sends \`init\` first, then \`joined\`, \`left\` and \`activity\` as they happen. Send \`set_activity\` to announce one of the kind's declared activities, and \`ping\` as plain text to get \`pong\`. Room kinds: ${presence.rooms.map((room) => `\`${room.kind}\``).join(", ")}.`,
|
|
30
|
+
verification: "public",
|
|
31
|
+
params: PresenceRoomParams,
|
|
32
|
+
versions: PRESENCE_ROOM_VERSIONS,
|
|
33
|
+
send: [
|
|
34
|
+
{ name: "init", summary: "Everyone in the room, sent once on connect.", schema: PresenceRoomInitFrame },
|
|
35
|
+
{ name: "joined", summary: "Another participant joined.", schema: PresenceJoinedFrame },
|
|
36
|
+
{ name: "left", summary: "Another participant left, or their socket dropped.", schema: PresenceLeftFrame },
|
|
37
|
+
{ name: "activity", summary: "Another participant changed what they are doing.", schema: PresenceActivityFrame },
|
|
38
|
+
{ name: "error", summary: "A frame the room could not accept.", schema: PresenceRoomErrorFrame },
|
|
39
|
+
{ name: "pong", summary: "The answer to `ping`, as plain text.", text: PresenceRoomPong.value },
|
|
40
|
+
],
|
|
41
|
+
receive: [
|
|
42
|
+
{ name: "set_activity", summary: "Announce a declared activity.", schema: PresenceSetActivityFrame },
|
|
43
|
+
{ name: "ping", summary: "The keepalive, as plain text.", text: PresenceRoomPing.value },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|