@forumone/throughline-core 0.9.1 → 0.10.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +192 -33
  3. package/bin/payload-cli.mjs +443 -0
  4. package/bin/throughline-payload.mjs +6 -0
  5. package/dist/audit/collection.d.ts +3 -0
  6. package/dist/audit/collection.d.ts.map +1 -1
  7. package/dist/audit/collection.js +2 -0
  8. package/dist/audit/collection.js.map +1 -1
  9. package/dist/env/index.d.ts +54 -0
  10. package/dist/env/index.d.ts.map +1 -0
  11. package/dist/env/index.js +113 -0
  12. package/dist/env/index.js.map +1 -0
  13. package/dist/index.d.ts +6 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp/api-key-access.d.ts +50 -0
  18. package/dist/mcp/api-key-access.d.ts.map +1 -0
  19. package/dist/mcp/api-key-access.js +94 -0
  20. package/dist/mcp/api-key-access.js.map +1 -0
  21. package/dist/mcp/index.d.ts +1 -0
  22. package/dist/mcp/index.d.ts.map +1 -1
  23. package/dist/mcp/index.js +1 -0
  24. package/dist/mcp/index.js.map +1 -1
  25. package/dist/observability/collection.d.ts +13 -0
  26. package/dist/observability/collection.d.ts.map +1 -0
  27. package/dist/observability/collection.js +76 -0
  28. package/dist/observability/collection.js.map +1 -0
  29. package/dist/observability/index.d.ts +9 -0
  30. package/dist/observability/index.d.ts.map +1 -0
  31. package/dist/observability/index.js +5 -0
  32. package/dist/observability/index.js.map +1 -0
  33. package/dist/observability/plugin.d.ts +21 -0
  34. package/dist/observability/plugin.d.ts.map +1 -0
  35. package/dist/observability/plugin.js +51 -0
  36. package/dist/observability/plugin.js.map +1 -0
  37. package/dist/observability/report.d.ts +175 -0
  38. package/dist/observability/report.d.ts.map +1 -0
  39. package/dist/observability/report.js +274 -0
  40. package/dist/observability/report.js.map +1 -0
  41. package/dist/observability/writer.d.ts +24 -0
  42. package/dist/observability/writer.d.ts.map +1 -0
  43. package/dist/observability/writer.js +72 -0
  44. package/dist/observability/writer.js.map +1 -0
  45. package/dist/testing/anonymousAccess.d.ts +111 -0
  46. package/dist/testing/anonymousAccess.d.ts.map +1 -0
  47. package/dist/testing/anonymousAccess.js +185 -0
  48. package/dist/testing/anonymousAccess.js.map +1 -0
  49. package/dist/testing/describeAnonymousAccess.d.ts +26 -0
  50. package/dist/testing/describeAnonymousAccess.d.ts.map +1 -0
  51. package/dist/testing/describeAnonymousAccess.js +76 -0
  52. package/dist/testing/describeAnonymousAccess.js.map +1 -0
  53. package/dist/testing/index.d.ts +11 -0
  54. package/dist/testing/index.d.ts.map +1 -0
  55. package/dist/testing/index.js +9 -0
  56. package/dist/testing/index.js.map +1 -0
  57. package/package.json +30 -7
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  export { AUDIT_ACTIONS, AUDIT_MCP_SERVERS, DEFAULT_AUDIT_SLUG, auditPlugin, createAuditCollection, createAuditWriter, getAuditWriter, } from './audit/index.js';
2
2
  export type { AuditAction, AuditActor, AuditCollectionOptions, AuditEventInput, AuditMcpServer, AuditPluginOptions, AuditWriter, AuditWriterOptions, } from './audit/index.js';
3
+ export { EnvironmentError, assertEnvironment, checkEnvValue } from './env/index.js';
4
+ export type { EnvValueProblem, EnvironmentCheck } from './env/index.js';
3
5
  export { createInngestClient } from './events/index.js';
4
6
  export type { CoreEvents, FrameworkEvents, InngestClientOptions } from './events/index.js';
5
- export { McpMetaSchema, auditContext, deniedEnvelope, createMcpToolCollector, toPayloadMcpTool, toPayloadMcpTools, withMeta, } from './mcp/index.js';
7
+ export { MCP_API_KEYS_SLUG, McpMetaSchema, auditContext, deniedEnvelope, createMcpToolCollector, isMcpApiKeyPrincipal, isSignedIn, mcpApiKeyAccess, signedIn, toPayloadMcpTool, toPayloadMcpTools, withMeta, } from './mcp/index.js';
6
8
  export type { AddToolsOptions, AuditContextFields, CreateMcpToolCollectorOptions, DeclareToolsOptions, McpMeta, McpToolCollector, McpToolDescriptor, PayloadMcpRequest, PayloadMcpTool, ToPayloadMcpToolOptions, } from './mcp/index.js';
7
9
  export { defaultLogger, createNamedLogger } from './logger/index.js';
10
+ export { DEFAULT_JOB_FAILURES_SLUG, DEFAULT_REPORTED_HEADERS, ERROR_WEBHOOK_URL_ENV, JOB_FAILURE_KINDS, NEVER_REPORTED_HEADERS, buildHealthcheckFailureReport, buildJobFailureReport, buildRequestErrorReport, clientPrefix, createErrorReporter, createJobFailureWriter, createJobFailuresCollection, currentDeployment, describeErrorReporting, getJobFailureWriter, jobFailuresPlugin, reportError, summariseReport, } from './observability/index.js';
11
+ export type { BuildJobFailureReportInput, BuildRequestErrorReportOptions, ErrorReport, ErrorReporter, ErrorReporterOptions, HealthcheckFailureReport, JobFailureKind, JobFailureReport, JobFailureWriter, JobFailureWriterOptions, JobFailuresCollectionOptions, JobFailuresPluginOptions, ReportDeployment, ReportableHeaders, RequestErrorReport, } from './observability/index.js';
8
12
  export { documentContentHash, formatZodIssues, sha256Hex, unwrapRelationshipId, } from './utils/index.js';
9
13
  export type { DocumentContentHashOptions } from './utils/index.js';
10
- export type { AuthenticatedUser, BaseCorePluginOptions, CorePlugin, Logger, McpToolContext, McpToolDefinition, PluginRegistry, PluginRegistryEntry, } from '@forumone/throughline-plugin-contract';
14
+ export type { AuthenticatedUser, BaseCorePluginOptions, CollectionPluginOptions, CorePlugin, EnvRequirement, Logger, McpToolContext, McpToolDefinition, PluginAdminGroup, PluginAdminOptions, PluginRegistry, PluginRegistryEntry, } from '@forumone/throughline-plugin-contract';
11
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE1F,OAAO,EACL,aAAa,EACb,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AACvB,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,6BAA6B,EAC7B,mBAAmB,EACnB,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,uBAAuB,GACxB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,oBAAoB,GACrB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AAIlE,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,GACpB,MAAM,uCAAuC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnF,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE1F,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,UAAU,EACV,eAAe,EACf,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AACvB,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,6BAA6B,EAC7B,mBAAmB,EACnB,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,uBAAuB,GACxB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,6BAA6B,EAC7B,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,2BAA2B,EAC3B,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,EACX,eAAe,GAChB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EACV,0BAA0B,EAC1B,8BAA8B,EAC9B,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,wBAAwB,EACxB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAC5B,wBAAwB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,oBAAoB,GACrB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AAIlE,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,UAAU,EACV,cAAc,EACd,MAAM,EACN,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,GACpB,MAAM,uCAAuC,CAAA"}
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  // Subsystems are re-exported as they are added during C4. The aggregated
2
2
  // surface here mirrors the package's `./<subpath>` exports.
3
3
  export { AUDIT_ACTIONS, AUDIT_MCP_SERVERS, DEFAULT_AUDIT_SLUG, auditPlugin, createAuditCollection, createAuditWriter, getAuditWriter, } from './audit/index.js';
4
+ export { EnvironmentError, assertEnvironment, checkEnvValue } from './env/index.js';
4
5
  export { createInngestClient } from './events/index.js';
5
- export { McpMetaSchema, auditContext, deniedEnvelope, createMcpToolCollector, toPayloadMcpTool, toPayloadMcpTools, withMeta, } from './mcp/index.js';
6
+ export { MCP_API_KEYS_SLUG, McpMetaSchema, auditContext, deniedEnvelope, createMcpToolCollector, isMcpApiKeyPrincipal, isSignedIn, mcpApiKeyAccess, signedIn, toPayloadMcpTool, toPayloadMcpTools, withMeta, } from './mcp/index.js';
6
7
  export { defaultLogger, createNamedLogger } from './logger/index.js';
8
+ export { DEFAULT_JOB_FAILURES_SLUG, DEFAULT_REPORTED_HEADERS, ERROR_WEBHOOK_URL_ENV, JOB_FAILURE_KINDS, NEVER_REPORTED_HEADERS, buildHealthcheckFailureReport, buildJobFailureReport, buildRequestErrorReport, clientPrefix, createErrorReporter, createJobFailureWriter, createJobFailuresCollection, currentDeployment, describeErrorReporting, getJobFailureWriter, jobFailuresPlugin, reportError, summariseReport, } from './observability/index.js';
7
9
  export { documentContentHash, formatZodIssues, sha256Hex, unwrapRelationshipId, } from './utils/index.js';
8
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,4DAA4D;AAE5D,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,EACL,aAAa,EACb,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AAcvB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,oBAAoB,GACrB,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,4DAA4D;AAE5D,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,GACf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,UAAU,EACV,eAAe,EACf,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AAcvB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,sBAAsB,EACtB,6BAA6B,EAC7B,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,2BAA2B,EAC3B,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,EACX,eAAe,GAChB,MAAM,0BAA0B,CAAA;AAmBjC,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,oBAAoB,GACrB,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,50 @@
1
+ import type { Access, CollectionConfig } from 'payload';
2
+ /** The slug `@payloadcms/plugin-mcp` gives its key collection. */
3
+ export declare const MCP_API_KEYS_SLUG = "payload-mcp-api-keys";
4
+ /** What Payload puts on `req.user`. */
5
+ export type AccessUser = Parameters<Access>[0]['req']['user'];
6
+ /**
7
+ * True when `req.user` is an MCP key document rather than a person.
8
+ *
9
+ * Only a request authenticated by Payload's REST API-key strategy against the
10
+ * key collection carries this principal — which Payload 3.89.0 and later no
11
+ * longer register. A tool call through `/api/mcp` arrives as the key's user.
12
+ */
13
+ export declare function isMcpApiKeyPrincipal(user: AccessUser | undefined): boolean;
14
+ /**
15
+ * A principal a site should treat as a signed-in person.
16
+ *
17
+ * Use this wherever an access rule would otherwise say `Boolean(req.user)` —
18
+ * which is the rule Payload's own `defaultAccess` uses. A bearer credential
19
+ * presented over REST is a `req.user` and is not somebody signed in.
20
+ */
21
+ export declare function isSignedIn(user: AccessUser | undefined): user is NonNullable<AccessUser>;
22
+ /** `isSignedIn` as a Payload `Access` function. */
23
+ export declare const signedIn: Access;
24
+ /**
25
+ * An `overrideApiKeyCollection` for `mcpPlugin` that makes the key collection
26
+ * admin-only: read, create, update, delete and unlock all answer to `isAdmin`.
27
+ *
28
+ * `isAdmin` is the site's own rule, since only the site knows what an
29
+ * administrator is. Whatever it says, an MCP key principal is refused first, so
30
+ * a key can never manage keys.
31
+ *
32
+ * It touches `access` and nothing else, so it composes with other overrides:
33
+ *
34
+ * ```ts
35
+ * const keysAdminOnly = mcpApiKeyAccess(isAdmin)
36
+ *
37
+ * mcpPlugin({
38
+ * mcp: { tools: mcpTools.tools },
39
+ * overrideApiKeyCollection: collection => {
40
+ * const hardened = keysAdminOnly(collection)
41
+ * return { ...hardened, admin: { ...hardened.admin, group: 'Throughline' } }
42
+ * },
43
+ * })
44
+ * ```
45
+ *
46
+ * The plugin's field-level rules on the key's `user` relationship are left as
47
+ * shipped: a new key is bound to whoever creates it.
48
+ */
49
+ export declare function mcpApiKeyAccess(isAdmin: Access): (collection: CollectionConfig) => CollectionConfig;
50
+ //# sourceMappingURL=api-key-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-access.d.ts","sourceRoot":"","sources":["../../src/mcp/api-key-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAiCvD,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,yBAAyB,CAAA;AAEvD,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAE1E;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,IAAI,WAAW,CAAC,UAAU,CAAC,CAExF;AAED,mDAAmD;AACnD,eAAO,MAAM,QAAQ,EAAE,MAA0C,CAAA;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,GACd,CAAC,UAAU,EAAE,gBAAgB,KAAK,gBAAgB,CAcpD"}
@@ -0,0 +1,94 @@
1
+ /*
2
+ Who may manage MCP keys, and who counts as signed in. Issue #157.
3
+
4
+ **The key collection.** `@payloadcms/plugin-mcp` brings `payload-mcp-api-keys`.
5
+ Older releases declared it with no `access` block, so every operation fell
6
+ through to Payload's `defaultAccess` — anybody signed in. Current ones scope each
7
+ person to their own keys. Neither is the right rule for a collection of
8
+ credentials: a key runs every tool as the person it is bound to, so who may mint,
9
+ read, rotate and revoke one is an administrator's decision. `mcpApiKeyAccess`
10
+ makes it one, through the plugin's `overrideApiKeyCollection`.
11
+
12
+ **The principal.** The collection sets `auth.useAPIKey: true`. Before Payload
13
+ 3.89.0 that registered Payload's API-key strategy globally, so
14
+ `Authorization: payload-mcp-api-keys API-Key <key>` was accepted on every REST
15
+ route and put the *key document* on `req.user`, tagged
16
+ `collection: 'payload-mcp-api-keys'`. It has no roles, so role checks denied it;
17
+ `Boolean(req.user)` did not. On a site whose draft rule was "published, or
18
+ anybody signed in", that served every draft to anyone holding a key, with no MCP
19
+ involved.
20
+
21
+ 3.89.0 excludes that slug from the strategy loop, and the suite's peer floor is
22
+ `^3.89.0` because of it. But one conditional in a dependency is not a control
23
+ anybody here would notice losing, so `isSignedIn` refuses the key principal
24
+ itself. Two things then have to fail for the hole to reopen.
25
+
26
+ **This cannot affect `/api/mcp`.** That endpoint resolves a presented key on its
27
+ own and runs the tool as the *user* the key points at, on the users collection,
28
+ with that person's roles. Only the REST strategy's principal carries the key
29
+ collection's slug, and that is the only principal refused here.
30
+ */
31
+ /** The slug `@payloadcms/plugin-mcp` gives its key collection. */
32
+ export const MCP_API_KEYS_SLUG = 'payload-mcp-api-keys';
33
+ /**
34
+ * True when `req.user` is an MCP key document rather than a person.
35
+ *
36
+ * Only a request authenticated by Payload's REST API-key strategy against the
37
+ * key collection carries this principal — which Payload 3.89.0 and later no
38
+ * longer register. A tool call through `/api/mcp` arrives as the key's user.
39
+ */
40
+ export function isMcpApiKeyPrincipal(user) {
41
+ return user?.collection === MCP_API_KEYS_SLUG;
42
+ }
43
+ /**
44
+ * A principal a site should treat as a signed-in person.
45
+ *
46
+ * Use this wherever an access rule would otherwise say `Boolean(req.user)` —
47
+ * which is the rule Payload's own `defaultAccess` uses. A bearer credential
48
+ * presented over REST is a `req.user` and is not somebody signed in.
49
+ */
50
+ export function isSignedIn(user) {
51
+ return Boolean(user) && !isMcpApiKeyPrincipal(user);
52
+ }
53
+ /** `isSignedIn` as a Payload `Access` function. */
54
+ export const signedIn = ({ req }) => isSignedIn(req.user);
55
+ /**
56
+ * An `overrideApiKeyCollection` for `mcpPlugin` that makes the key collection
57
+ * admin-only: read, create, update, delete and unlock all answer to `isAdmin`.
58
+ *
59
+ * `isAdmin` is the site's own rule, since only the site knows what an
60
+ * administrator is. Whatever it says, an MCP key principal is refused first, so
61
+ * a key can never manage keys.
62
+ *
63
+ * It touches `access` and nothing else, so it composes with other overrides:
64
+ *
65
+ * ```ts
66
+ * const keysAdminOnly = mcpApiKeyAccess(isAdmin)
67
+ *
68
+ * mcpPlugin({
69
+ * mcp: { tools: mcpTools.tools },
70
+ * overrideApiKeyCollection: collection => {
71
+ * const hardened = keysAdminOnly(collection)
72
+ * return { ...hardened, admin: { ...hardened.admin, group: 'Throughline' } }
73
+ * },
74
+ * })
75
+ * ```
76
+ *
77
+ * The plugin's field-level rules on the key's `user` relationship are left as
78
+ * shipped: a new key is bound to whoever creates it.
79
+ */
80
+ export function mcpApiKeyAccess(isAdmin) {
81
+ const adminOnly = args => (isMcpApiKeyPrincipal(args.req.user) ? false : isAdmin(args));
82
+ return collection => ({
83
+ ...collection,
84
+ access: {
85
+ ...collection.access,
86
+ read: adminOnly,
87
+ create: adminOnly,
88
+ update: adminOnly,
89
+ delete: adminOnly,
90
+ unlock: adminOnly,
91
+ },
92
+ });
93
+ }
94
+ //# sourceMappingURL=api-key-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-access.js","sourceRoot":"","sources":["../../src/mcp/api-key-access.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6BE;AAEF,kEAAkE;AAClE,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAA;AAKvD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAA4B;IAC/D,OAAQ,IAAoD,EAAE,UAAU,KAAK,iBAAiB,CAAA;AAChG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAA4B;IACrD,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;AACrD,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,QAAQ,GAAW,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe;IAEf,MAAM,SAAS,GAAW,IAAI,CAAC,EAAE,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAE/F,OAAO,UAAU,CAAC,EAAE,CAAC,CAAC;QACpB,GAAG,UAAU;QACb,MAAM,EAAE;YACN,GAAG,UAAU,CAAC,MAAM;YACpB,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,SAAS;SAClB;KACF,CAAC,CAAA;AACJ,CAAC"}
@@ -8,4 +8,5 @@ export { createMcpToolCollector } from './collector.js';
8
8
  export type { AddToolsOptions, CreateMcpToolCollectorOptions, DeclareToolsOptions, McpToolCollector, McpToolDescriptor, } from './collector.js';
9
9
  export { deniedEnvelope } from './envelope.js';
10
10
  export { auditServerFor, mcpServerRefusal } from './audit-server.js';
11
+ export { MCP_API_KEYS_SLUG, isMcpApiKeyPrincipal, isSignedIn, mcpApiKeyAccess, signedIn, } from './api-key-access.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACnD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACtE,YAAY,EACV,iBAAiB,EACjB,cAAc,EACd,uBAAuB,GACxB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EACV,eAAe,EACf,6BAA6B,EAC7B,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACnD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACtE,YAAY,EACV,iBAAiB,EACjB,cAAc,EACd,uBAAuB,GACxB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EACV,eAAe,EACf,6BAA6B,EAC7B,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,eAAe,EACf,QAAQ,GACT,MAAM,qBAAqB,CAAA"}
package/dist/mcp/index.js CHANGED
@@ -4,4 +4,5 @@ export { toPayloadMcpTool, toPayloadMcpTools } from './payload-mcp.js';
4
4
  export { createMcpToolCollector } from './collector.js';
5
5
  export { deniedEnvelope } from './envelope.js';
6
6
  export { auditServerFor, mcpServerRefusal } from './audit-server.js';
7
+ export { MCP_API_KEYS_SLUG, isMcpApiKeyPrincipal, isSignedIn, mcpApiKeyAccess, signedIn, } from './api-key-access.js';
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGjD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAOtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AASvD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGjD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAOtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AASvD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,eAAe,EACf,QAAQ,GACT,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,13 @@
1
+ import type { Access, CollectionConfig } from 'payload';
2
+ export declare const DEFAULT_JOB_FAILURES_SLUG = "job-failures";
3
+ /** What kind of thing failed. Adding a value is a schema change and needs a migration. */
4
+ export declare const JOB_FAILURE_KINDS: readonly ["job", "healthcheck"];
5
+ export type JobFailureKind = (typeof JOB_FAILURE_KINDS)[number];
6
+ export interface JobFailuresCollectionOptions {
7
+ /** Override the collection slug. Default: `'job-failures'`. */
8
+ slug?: string;
9
+ /** Read access. Default: users with the `admin` role. */
10
+ readAccess?: Access;
11
+ }
12
+ export declare function createJobFailuresCollection(options?: JobFailuresCollectionOptions): CollectionConfig;
13
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/observability/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAgBvD,eAAO,MAAM,yBAAyB,iBAAiB,CAAA;AAEvD,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,iCAAkC,CAAA;AAChE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/D,MAAM,WAAW,4BAA4B;IAC3C,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAOD,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,4BAAiC,GACzC,gBAAgB,CAuDlB"}
@@ -0,0 +1,76 @@
1
+ /*
2
+ Where a background job's failure is recorded.
3
+
4
+ Deliberately not the audit log. `audit-events` records who did what through
5
+ an MCP tool — `mcpServer` and `mcpTool` are `NOT NULL`, and every row answers
6
+ "which tool did this". A cron that ran out of retries is not an action anybody
7
+ took; it is an operational fact, with its own fields (a run id, a triggering
8
+ event) and its own lifetime (worth keeping for weeks, not forever). Giving it
9
+ a fake MCP server name would make that column mean nothing.
10
+
11
+ Read-only from outside the writer, like the audit log: the writer uses the
12
+ Local API, which bypasses these access rules.
13
+ */
14
+ export const DEFAULT_JOB_FAILURES_SLUG = 'job-failures';
15
+ /** What kind of thing failed. Adding a value is a schema change and needs a migration. */
16
+ export const JOB_FAILURE_KINDS = ['job', 'healthcheck'];
17
+ const adminOnly = ({ req }) => {
18
+ const roles = req.user?.['roles'] ?? [];
19
+ return roles.includes('admin');
20
+ };
21
+ export function createJobFailuresCollection(options = {}) {
22
+ return {
23
+ slug: options.slug ?? DEFAULT_JOB_FAILURES_SLUG,
24
+ admin: {
25
+ useAsTitle: 'summary',
26
+ defaultColumns: ['createdAt', 'kind', 'source', 'message'],
27
+ description: 'Background jobs that ran out of retries, and healthchecks that failed. Read-only; written by the failure handlers.',
28
+ },
29
+ access: {
30
+ read: options.readAccess ?? adminOnly,
31
+ create: () => false,
32
+ update: () => false,
33
+ delete: () => false,
34
+ },
35
+ fields: [
36
+ {
37
+ name: 'createdAt',
38
+ type: 'date',
39
+ required: true,
40
+ defaultValue: () => new Date().toISOString(),
41
+ admin: { position: 'sidebar', readOnly: true },
42
+ },
43
+ {
44
+ name: 'kind',
45
+ type: 'select',
46
+ required: true,
47
+ options: JOB_FAILURE_KINDS.map((value) => ({ label: value, value })),
48
+ },
49
+ {
50
+ name: 'source',
51
+ type: 'text',
52
+ required: true,
53
+ admin: { description: 'The function or check that failed, e.g. an Inngest function id.' },
54
+ },
55
+ { name: 'summary', type: 'text', required: true },
56
+ { name: 'message', type: 'textarea', required: true },
57
+ { name: 'errorName', type: 'text' },
58
+ {
59
+ name: 'runId',
60
+ type: 'text',
61
+ admin: { description: "The run's id in the job runner, for finding it in its dashboard." },
62
+ },
63
+ { name: 'triggerEvent', type: 'text' },
64
+ {
65
+ name: 'details',
66
+ type: 'json',
67
+ admin: { description: 'For a healthcheck: every failed check and what it said.' },
68
+ },
69
+ { name: 'environment', type: 'text' },
70
+ { name: 'commit', type: 'text' },
71
+ ],
72
+ indexes: [{ fields: ['createdAt'] }, { fields: ['source', 'createdAt'] }],
73
+ timestamps: false,
74
+ };
75
+ }
76
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/observability/collection.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;EAYE;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,cAAc,CAAA;AAEvD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,CAAU,CAAA;AAUhE,MAAM,SAAS,GAAW,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;IACpC,MAAM,KAAK,GAAI,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAA0B,IAAI,EAAE,CAAA;IACjE,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC,CAAA;AAED,MAAM,UAAU,2BAA2B,CACzC,UAAwC,EAAE;IAE1C,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,yBAAyB;QAC/C,KAAK,EAAE;YACL,UAAU,EAAE,SAAS;YACrB,cAAc,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC;YAC1D,WAAW,EACT,oHAAoH;SACvH;QACD,MAAM,EAAE;YACN,IAAI,EAAE,OAAO,CAAC,UAAU,IAAI,SAAS;YACrC,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;YACnB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;YACnB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;SACpB;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,IAAI;gBACd,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBAC5C,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;aAC/C;YACD;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,QAAQ;gBACd,QAAQ,EAAE,IAAI;gBACd,OAAO,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;aACrE;YACD;gBACE,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,IAAI;gBACd,KAAK,EAAE,EAAE,WAAW,EAAE,iEAAiE,EAAE;aAC1F;YACD,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;YACjD,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;YACrD,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE;YACnC;gBACE,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,MAAM;gBACZ,KAAK,EAAE,EAAE,WAAW,EAAE,kEAAkE,EAAE;aAC3F;YACD,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE;YACtC;gBACE,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE,MAAM;gBACZ,KAAK,EAAE,EAAE,WAAW,EAAE,yDAAyD,EAAE;aAClF;YACD,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE;YACrC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE;SACjC;QACD,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC;QACzE,UAAU,EAAE,KAAK;KAClB,CAAA;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { DEFAULT_REPORTED_HEADERS, ERROR_WEBHOOK_URL_ENV, NEVER_REPORTED_HEADERS, buildHealthcheckFailureReport, buildJobFailureReport, buildRequestErrorReport, clientPrefix, createErrorReporter, currentDeployment, describeErrorReporting, reportError, summariseReport, } from './report.js';
2
+ export type { BuildJobFailureReportInput, BuildRequestErrorReportOptions, ErrorReport, ErrorReporter, ErrorReporterOptions, HealthcheckFailureReport, JobFailureReport, ReportDeployment, ReportableHeaders, RequestErrorReport, } from './report.js';
3
+ export { DEFAULT_JOB_FAILURES_SLUG, JOB_FAILURE_KINDS, createJobFailuresCollection, } from './collection.js';
4
+ export type { JobFailureKind, JobFailuresCollectionOptions } from './collection.js';
5
+ export { createJobFailureWriter } from './writer.js';
6
+ export type { JobFailureWriter, JobFailureWriterOptions } from './writer.js';
7
+ export { getJobFailureWriter, jobFailuresPlugin } from './plugin.js';
8
+ export type { JobFailuresPluginOptions } from './plugin.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,6BAA6B,EAC7B,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,WAAW,EACX,eAAe,GAChB,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,0BAA0B,EAC1B,8BAA8B,EAC9B,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,cAAc,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAEnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACpD,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAE5E,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpE,YAAY,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_REPORTED_HEADERS, ERROR_WEBHOOK_URL_ENV, NEVER_REPORTED_HEADERS, buildHealthcheckFailureReport, buildJobFailureReport, buildRequestErrorReport, clientPrefix, createErrorReporter, currentDeployment, describeErrorReporting, reportError, summariseReport, } from './report.js';
2
+ export { DEFAULT_JOB_FAILURES_SLUG, JOB_FAILURE_KINDS, createJobFailuresCollection, } from './collection.js';
3
+ export { createJobFailureWriter } from './writer.js';
4
+ export { getJobFailureWriter, jobFailuresPlugin } from './plugin.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/observability/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,6BAA6B,EAC7B,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,WAAW,EACX,eAAe,GAChB,MAAM,aAAa,CAAA;AAcpB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { type BaseCorePluginOptions, type CorePlugin } from '@forumone/throughline-plugin-contract';
2
+ import { type JobFailuresCollectionOptions } from './collection.js';
3
+ import { type JobFailureWriter } from './writer.js';
4
+ export interface JobFailuresPluginOptions extends BaseCorePluginOptions, JobFailuresCollectionOptions {
5
+ }
6
+ /**
7
+ * Adds the `job-failures` collection and attaches its writer to the Payload
8
+ * instance, where `getJobFailureWriter` — and the workflows package's failure
9
+ * handlers — find it.
10
+ */
11
+ export declare const jobFailuresPlugin: CorePlugin<JobFailuresPluginOptions>;
12
+ /**
13
+ * The writer `jobFailuresPlugin` attached, or `undefined` when the plugin is
14
+ * not registered.
15
+ *
16
+ * Unlike `getAuditWriter` this does not throw: it is called from failure
17
+ * handlers, which fall back to logging and reporting when there is nowhere
18
+ * to write.
19
+ */
20
+ export declare function getJobFailureWriter(payload: object): JobFailureWriter | undefined;
21
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/observability/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EAEhB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAA+B,KAAK,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAChG,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAO3E,MAAM,WAAW,wBACf,SAAQ,qBAAqB,EAAE,4BAA4B;CAAG;AAEhE;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAAU,CAAC,wBAAwB,CA4BhE,CAAA;AAEH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEjF"}
@@ -0,0 +1,51 @@
1
+ import { getPluginRegistry, } from '@forumone/throughline-plugin-contract';
2
+ import { createJobFailuresCollection } from './collection.js';
3
+ import { createJobFailureWriter } from './writer.js';
4
+ import { defaultLogger } from '../logger/index.js';
5
+ const PLUGIN_ID = '@forumone/throughline-core/observability';
6
+ const PLUGIN_VERSION = '0.1.0';
7
+ const WRITER_SYMBOL = Symbol.for('@forumone/throughline/job-failure-writer');
8
+ /**
9
+ * Adds the `job-failures` collection and attaches its writer to the Payload
10
+ * instance, where `getJobFailureWriter` — and the workflows package's failure
11
+ * handlers — find it.
12
+ */
13
+ export const jobFailuresPlugin = (options) => (incomingConfig) => {
14
+ if (options.enabled === false)
15
+ return incomingConfig;
16
+ return {
17
+ ...incomingConfig,
18
+ collections: [...(incomingConfig.collections ?? []), createJobFailuresCollection(options)],
19
+ onInit: async (payload) => {
20
+ if (incomingConfig.onInit)
21
+ await incomingConfig.onInit(payload);
22
+ Object.defineProperty(payload, WRITER_SYMBOL, {
23
+ value: createJobFailureWriter({
24
+ payload,
25
+ collectionSlug: options.slug,
26
+ logger: options.logger ?? defaultLogger,
27
+ }),
28
+ enumerable: false,
29
+ writable: false,
30
+ configurable: false,
31
+ });
32
+ getPluginRegistry(payload).register({
33
+ id: PLUGIN_ID,
34
+ version: PLUGIN_VERSION,
35
+ capabilities: ['job-failures'],
36
+ });
37
+ },
38
+ };
39
+ };
40
+ /**
41
+ * The writer `jobFailuresPlugin` attached, or `undefined` when the plugin is
42
+ * not registered.
43
+ *
44
+ * Unlike `getAuditWriter` this does not throw: it is called from failure
45
+ * handlers, which fall back to logging and reporting when there is nowhere
46
+ * to write.
47
+ */
48
+ export function getJobFailureWriter(payload) {
49
+ return payload[WRITER_SYMBOL];
50
+ }
51
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/observability/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,iBAAiB,GAClB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,2BAA2B,EAAqC,MAAM,iBAAiB,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAyB,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,MAAM,SAAS,GAAG,0CAA0C,CAAA;AAC5D,MAAM,cAAc,GAAG,OAAO,CAAA;AAC9B,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAA;AAK5E;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,cAAc,EAAE,EAAE;IAC9B,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO,cAAc,CAAA;IAEpD,OAAO;QACL,GAAG,cAAc;QACjB,WAAW,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,2BAA2B,CAAC,OAAO,CAAC,CAAC;QAC1F,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACxB,IAAI,cAAc,CAAC,MAAM;gBAAE,MAAM,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAE/D,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE;gBAC5C,KAAK,EAAE,sBAAsB,CAAC;oBAC5B,OAAO;oBACP,cAAc,EAAE,OAAO,CAAC,IAAI;oBAC5B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,aAAa;iBACxC,CAAC;gBACF,UAAU,EAAE,KAAK;gBACjB,QAAQ,EAAE,KAAK;gBACf,YAAY,EAAE,KAAK;aACpB,CAAC,CAAA;YAEF,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;gBAClC,EAAE,EAAE,SAAS;gBACb,OAAO,EAAE,cAAc;gBACvB,YAAY,EAAE,CAAC,cAAc,CAAC;aAC/B,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC,CAAA;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,OAAQ,OAAmC,CAAC,aAAa,CAAiC,CAAA;AAC5F,CAAC"}
@@ -0,0 +1,175 @@
1
+ import type { Logger } from '@forumone/throughline-plugin-contract';
2
+ /** Request headers copied into a report unless the caller names its own list. */
3
+ export declare const DEFAULT_REPORTED_HEADERS: readonly string[];
4
+ /**
5
+ * Headers that are never copied, even when a caller lists them. Each one is,
6
+ * or routinely carries, a credential. `x-forwarded-for` is here too: the whole
7
+ * address is personal data, and a report carries only its prefix.
8
+ */
9
+ export declare const NEVER_REPORTED_HEADERS: readonly string[];
10
+ /** The environment variable the default reporter reads. */
11
+ export declare const ERROR_WEBHOOK_URL_ENV = "ERROR_WEBHOOK_URL";
12
+ /** Which deployment produced a report, so it is attributable after a rollback. */
13
+ export interface ReportDeployment {
14
+ environment?: string;
15
+ commit?: string;
16
+ }
17
+ /** An unhandled error in a request, as `instrumentation.ts` sees one. */
18
+ export interface RequestErrorReport {
19
+ kind: 'request';
20
+ /** ISO 8601, so a report is orderable without trusting the receiver's clock. */
21
+ at: string;
22
+ message: string;
23
+ name?: string;
24
+ stack?: string;
25
+ /** React's error digest — the only handle on a minified Server Component error. */
26
+ digest?: string;
27
+ request: {
28
+ path: string;
29
+ method: string;
30
+ /** The allowlisted headers that were present, lower-cased. */
31
+ headers: Record<string, string>;
32
+ /** The client address reduced to a /16 (IPv4) or /64 (IPv6). */
33
+ clientPrefix?: string;
34
+ };
35
+ context: {
36
+ routePath?: string;
37
+ routeType?: string;
38
+ renderSource?: string;
39
+ revalidateReason?: string;
40
+ };
41
+ deployment: ReportDeployment;
42
+ }
43
+ /** A background job that ran out of retries. */
44
+ export interface JobFailureReport {
45
+ kind: 'job';
46
+ at: string;
47
+ /** The function that failed, e.g. an Inngest function id. `'unknown'` if nothing named it. */
48
+ functionId: string;
49
+ /** The run's id — the handle for finding it in the job runner's dashboard. */
50
+ runId?: string;
51
+ /** The event that triggered the run, so a failure is traceable to a cause. */
52
+ triggerEvent?: string;
53
+ message: string;
54
+ name?: string;
55
+ stack?: string;
56
+ deployment: ReportDeployment;
57
+ }
58
+ /** A healthcheck run in which at least one check failed. */
59
+ export interface HealthcheckFailureReport {
60
+ kind: 'healthcheck';
61
+ at: string;
62
+ functionId: string;
63
+ failed: Array<{
64
+ name: string;
65
+ details?: string;
66
+ }>;
67
+ deployment: ReportDeployment;
68
+ }
69
+ export type ErrorReport = RequestErrorReport | JobFailureReport | HealthcheckFailureReport;
70
+ /** Request headers as Next hands them over, or a Fetch `Headers`. */
71
+ export type ReportableHeaders = Record<string, string | string[] | undefined> | {
72
+ get(name: string): string | null;
73
+ };
74
+ export interface BuildRequestErrorReportOptions {
75
+ /** Headers to copy. Default: {@link DEFAULT_REPORTED_HEADERS}. {@link NEVER_REPORTED_HEADERS} are dropped regardless. */
76
+ headers?: readonly string[];
77
+ /** Default: read from the environment — see {@link currentDeployment}. */
78
+ deployment?: ReportDeployment;
79
+ now?: Date;
80
+ }
81
+ /**
82
+ * The deployment a report came from, read from the environment.
83
+ *
84
+ * `VERCEL_ENV` before `NODE_ENV`, because on Vercel a preview deployment is
85
+ * `NODE_ENV=production` too, and "production or somebody's branch" is the
86
+ * first question about an alert. Off Vercel, `NODE_ENV` is what there is.
87
+ */
88
+ export declare function currentDeployment(env?: NodeJS.ProcessEnv): ReportDeployment;
89
+ /**
90
+ * An IPv4 or IPv6 address reduced to a prefix: enough to tell one host's burst
91
+ * from many readers hitting the same fault, and not enough to identify anyone.
92
+ * `x-forwarded-for` may carry a list; only the first entry is the client.
93
+ */
94
+ export declare function clientPrefix(forwardedFor: string | undefined): string | undefined;
95
+ /**
96
+ * Build a report for an error captured during a request. Pure, so the
97
+ * redaction is testable without a server.
98
+ *
99
+ * `error` is `unknown` because Next types it that way and means it: the value
100
+ * reaching `onRequestError` may have been processed by React rather than being
101
+ * the instance that was thrown, which is why `digest` is read separately.
102
+ */
103
+ export declare function buildRequestErrorReport(error: unknown, request: {
104
+ path: string;
105
+ method: string;
106
+ headers: ReportableHeaders;
107
+ }, context?: object, options?: BuildRequestErrorReportOptions): RequestErrorReport;
108
+ export interface BuildJobFailureReportInput {
109
+ /** The failing function. Omitted or empty is reported as `'unknown'` rather than dropped. */
110
+ functionId?: string | undefined;
111
+ runId?: string | undefined;
112
+ triggerEvent?: string | undefined;
113
+ error: unknown;
114
+ }
115
+ /**
116
+ * Build a report for a job that ran out of retries.
117
+ *
118
+ * The stack is included: this goes to a webhook the team owns, where a stack
119
+ * is the most useful field in it. The job-failures collection, which admins
120
+ * read through the CMS, stores the message and not the stack.
121
+ */
122
+ export declare function buildJobFailureReport(input: BuildJobFailureReportInput, options?: {
123
+ deployment?: ReportDeployment;
124
+ now?: Date;
125
+ }): JobFailureReport;
126
+ /** Build a report for a healthcheck run with failing checks. */
127
+ export declare function buildHealthcheckFailureReport(failures: ReadonlyArray<{
128
+ name: string;
129
+ details?: string | undefined;
130
+ }>, options?: {
131
+ functionId?: string;
132
+ deployment?: ReportDeployment;
133
+ now?: Date;
134
+ }): HealthcheckFailureReport;
135
+ /**
136
+ * One line describing a report, for a receiver that renders rather than
137
+ * indexes. Reads the report structurally, so a site's own report shapes get a
138
+ * sensible line too.
139
+ */
140
+ export declare function summariseReport(report: unknown): string;
141
+ /**
142
+ * Whether reports go anywhere, as a sentence for a boot-time log line. A
143
+ * sentence rather than a boolean because "off because this is a laptop" and
144
+ * "off because somebody forgot in production" look identical otherwise.
145
+ */
146
+ export declare function describeErrorReporting(url?: string | undefined): string;
147
+ export interface ErrorReporterOptions {
148
+ /**
149
+ * Where to POST. Default: `process.env.ERROR_WEBHOOK_URL`, read on every
150
+ * call so a reporter built before the environment loads still finds it.
151
+ * Unset or empty means reports are not delivered.
152
+ */
153
+ url?: string | undefined;
154
+ /** Extra headers sent *to the webhook*, e.g. an `authorization` the receiver expects. */
155
+ webhookHeaders?: Record<string, string>;
156
+ /** Abort the POST after this long. Default: 3000ms — shorter than any function timeout. */
157
+ timeoutMs?: number;
158
+ /** Where a failed delivery is logged. Default: the console logger. */
159
+ logger?: Logger;
160
+ /** Injected for tests. Default: the global `fetch`. */
161
+ fetch?: typeof fetch;
162
+ }
163
+ /** Posts a report. Never throws, never rejects. */
164
+ export type ErrorReporter = (report: unknown) => Promise<void>;
165
+ /**
166
+ * A reporter that POSTs each report as JSON, with a `text` summary added.
167
+ *
168
+ * A delivery that fails — a refused connection, a timeout, a non-2xx — is
169
+ * logged as a warning and otherwise swallowed. It is never rethrown: see the
170
+ * note at the top of this file.
171
+ */
172
+ export declare function createErrorReporter(options?: ErrorReporterOptions): ErrorReporter;
173
+ /** The default reporter: `ERROR_WEBHOOK_URL`, a 3s timeout, the console logger. */
174
+ export declare const reportError: ErrorReporter;
175
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/observability/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uCAAuC,CAAA;AA8BnE,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAKrD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAOnD,CAAA;AAED,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,sBAAsB,CAAA;AAIxD,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAA;IACf,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAA;QACZ,MAAM,EAAE,MAAM,CAAA;QACd,8DAA8D;QAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC/B,gEAAgE;QAChE,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,OAAO,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,UAAU,EAAE,gBAAgB,CAAA;CAC7B;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,KAAK,CAAA;IACX,EAAE,EAAE,MAAM,CAAA;IACV,8FAA8F;IAC9F,UAAU,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,gBAAgB,CAAA;CAC7B;AAED,4DAA4D;AAC5D,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAA;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjD,UAAU,EAAE,gBAAgB,CAAA;CAC7B;AAED,MAAM,MAAM,WAAW,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,wBAAwB,CAAA;AAE1F,qEAAqE;AACrE,MAAM,MAAM,iBAAiB,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAEtF,MAAM,WAAW,8BAA8B;IAC7C,yHAAyH;IACzH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAOxF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAYjF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,EACrE,OAAO,GAAE,MAAW,EACpB,OAAO,GAAE,8BAAmC,GAC3C,kBAAkB,CAkCpB;AAED,MAAM,WAAW,0BAA0B;IACzC,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,0BAA0B,EACjC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAA;CAAO,GAC1D,gBAAgB,CAUlB;AAED,gEAAgE;AAChE,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,EACvE,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAA;CAAO,GAC/E,wBAAwB,CAW1B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAsBvD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAM,GAAG,SAA4B,GAAG,MAAM,CASzF;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAED,mDAAmD;AACnD,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE9D;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAqCrF;AAED,mFAAmF;AACnF,eAAO,MAAM,WAAW,EAAE,aAAqC,CAAA"}