@gate-forge/pack-webhook 0.1.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/LICENSE +202 -0
- package/dist/adapter-schema.d.ts +87 -0
- package/dist/adapter-schema.d.ts.map +1 -0
- package/dist/adapter-schema.js +75 -0
- package/dist/adapter-schema.js.map +1 -0
- package/dist/detector.d.ts +42 -0
- package/dist/detector.d.ts.map +1 -0
- package/dist/detector.js +361 -0
- package/dist/detector.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/obligations.d.ts +58 -0
- package/dist/obligations.d.ts.map +1 -0
- package/dist/obligations.js +64 -0
- package/dist/obligations.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 UKD
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity-adapter schema (pack deliverable: "entity-adapter schema doc").
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `@gate-forge/pack-sqlalchemy`'s adapter contract, scoped to
|
|
5
|
+
* the webhook pack's delivery-log resources. The witness service loads
|
|
6
|
+
* `.gateforge/adapters/<resourceId>.mjs` and calls `read(ctx, id)`
|
|
7
|
+
* when a test asks for persistence evidence (the
|
|
8
|
+
* `webhook:replay-idempotent` and `webhook:retry-bounded` contracts
|
|
9
|
+
* both need to read the in-memory delivery log to assert
|
|
10
|
+
* "exactly one side effect" and "bounded retry count").
|
|
11
|
+
*
|
|
12
|
+
* Adapter responsibilities (mirrored by {@link WebhookEntityAdapterSchema}
|
|
13
|
+
* and enforced by {@link validateWebhookEntityAdapter}):
|
|
14
|
+
*
|
|
15
|
+
* - `resourceId` — the resource this adapter proves; must match the
|
|
16
|
+
* file name (`<resourceId>.mjs`) so the registry binds by filename.
|
|
17
|
+
* - `read(ctx, id)` — GET-only fetch of one delivery record; returns
|
|
18
|
+
* the raw response body. Implementations MUST NOT mutate anything.
|
|
19
|
+
* - `normalize(body)` — project the raw body onto the evidence shape
|
|
20
|
+
* `{ entityId, fields }` where `fields` carry the classified primary
|
|
21
|
+
* key columns (`event_id`, `attempt`).
|
|
22
|
+
* - `deletion: 'hard' | 'archive'` — how removal manifests. Delivery
|
|
23
|
+
* records are append-only; archive semantics ("the row keeps
|
|
24
|
+
* existing retrievable") is the right mode for an audit log.
|
|
25
|
+
* - `environmentFingerprint` — the target-environment marker (e.g.
|
|
26
|
+
* the value of a response header the witness compares on a probe
|
|
27
|
+
* GET). The example webhook server emits
|
|
28
|
+
* `x-gateforge-env: example-webhook-v1`.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
/** The evidence shape `normalize` must return. */
|
|
32
|
+
export interface NormalizedEntity {
|
|
33
|
+
/** The entity id, extracted from the raw body (column-keyed). */
|
|
34
|
+
entityId: string;
|
|
35
|
+
/** Projected fields the obligation's `expectFields` can match. */
|
|
36
|
+
fields: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/** Context handed to `read` by the witness service. */
|
|
39
|
+
export interface EntityAdapterContext {
|
|
40
|
+
/** Base URL of the target environment (loopback, trusted). */
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
/** Per-request headers (e.g. the run token for authenticity). */
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
/** The frozen adapter module contract. */
|
|
46
|
+
export interface WebhookEntityAdapter {
|
|
47
|
+
resourceId: string;
|
|
48
|
+
read(ctx: EntityAdapterContext, id: string): Promise<unknown>;
|
|
49
|
+
normalize(body: unknown): NormalizedEntity;
|
|
50
|
+
deletion: 'hard' | 'archive';
|
|
51
|
+
environmentFingerprint: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Zod schema for the data fields of an adapter module (functions are
|
|
55
|
+
* checked by {@link validateWebhookEntityAdapter} for a single-cause
|
|
56
|
+
* diagnostic list; zod function schemas would blur the message).
|
|
57
|
+
*/
|
|
58
|
+
export declare const WebhookEntityAdapterSchema: z.ZodObject<{
|
|
59
|
+
resourceId: z.ZodString;
|
|
60
|
+
deletion: z.ZodEnum<{
|
|
61
|
+
hard: "hard";
|
|
62
|
+
archive: "archive";
|
|
63
|
+
}>;
|
|
64
|
+
environmentFingerprint: z.ZodString;
|
|
65
|
+
read: z.ZodUnknown;
|
|
66
|
+
normalize: z.ZodUnknown;
|
|
67
|
+
}, z.core.$strict>;
|
|
68
|
+
/** Outcome of {@link validateWebhookEntityAdapter}. */
|
|
69
|
+
export type WebhookEntityAdapterValidation = {
|
|
70
|
+
ok: true;
|
|
71
|
+
adapter: WebhookEntityAdapter;
|
|
72
|
+
} | {
|
|
73
|
+
ok: false;
|
|
74
|
+
issues: string[];
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Validates an adapter module against the frozen contract, failing
|
|
78
|
+
* closed with one issue per violated field.
|
|
79
|
+
*
|
|
80
|
+
* Args:
|
|
81
|
+
* value: The default export of an `.mjs` adapter module.
|
|
82
|
+
*
|
|
83
|
+
* Returns:
|
|
84
|
+
* WebhookEntityAdapterValidation: The typed adapter, or every issue found.
|
|
85
|
+
*/
|
|
86
|
+
export declare function validateWebhookEntityAdapter(value: unknown): WebhookEntityAdapterValidation;
|
|
87
|
+
//# sourceMappingURL=adapter-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-schema.d.ts","sourceRoot":"","sources":["../src/adapter-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,GAAG,EAAE,oBAAoB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAC3C,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;kBAQ5B,CAAC;AAEZ,uDAAuD;AACvD,MAAM,MAAM,8BAA8B,GACtC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAC3C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEpC;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,8BAA8B,CAkB3F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity-adapter schema (pack deliverable: "entity-adapter schema doc").
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `@gate-forge/pack-sqlalchemy`'s adapter contract, scoped to
|
|
5
|
+
* the webhook pack's delivery-log resources. The witness service loads
|
|
6
|
+
* `.gateforge/adapters/<resourceId>.mjs` and calls `read(ctx, id)`
|
|
7
|
+
* when a test asks for persistence evidence (the
|
|
8
|
+
* `webhook:replay-idempotent` and `webhook:retry-bounded` contracts
|
|
9
|
+
* both need to read the in-memory delivery log to assert
|
|
10
|
+
* "exactly one side effect" and "bounded retry count").
|
|
11
|
+
*
|
|
12
|
+
* Adapter responsibilities (mirrored by {@link WebhookEntityAdapterSchema}
|
|
13
|
+
* and enforced by {@link validateWebhookEntityAdapter}):
|
|
14
|
+
*
|
|
15
|
+
* - `resourceId` — the resource this adapter proves; must match the
|
|
16
|
+
* file name (`<resourceId>.mjs`) so the registry binds by filename.
|
|
17
|
+
* - `read(ctx, id)` — GET-only fetch of one delivery record; returns
|
|
18
|
+
* the raw response body. Implementations MUST NOT mutate anything.
|
|
19
|
+
* - `normalize(body)` — project the raw body onto the evidence shape
|
|
20
|
+
* `{ entityId, fields }` where `fields` carry the classified primary
|
|
21
|
+
* key columns (`event_id`, `attempt`).
|
|
22
|
+
* - `deletion: 'hard' | 'archive'` — how removal manifests. Delivery
|
|
23
|
+
* records are append-only; archive semantics ("the row keeps
|
|
24
|
+
* existing retrievable") is the right mode for an audit log.
|
|
25
|
+
* - `environmentFingerprint` — the target-environment marker (e.g.
|
|
26
|
+
* the value of a response header the witness compares on a probe
|
|
27
|
+
* GET). The example webhook server emits
|
|
28
|
+
* `x-gateforge-env: example-webhook-v1`.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
/**
|
|
32
|
+
* Zod schema for the data fields of an adapter module (functions are
|
|
33
|
+
* checked by {@link validateWebhookEntityAdapter} for a single-cause
|
|
34
|
+
* diagnostic list; zod function schemas would blur the message).
|
|
35
|
+
*/
|
|
36
|
+
export const WebhookEntityAdapterSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
resourceId: z.string().min(1),
|
|
39
|
+
deletion: z.enum(['hard', 'archive']),
|
|
40
|
+
environmentFingerprint: z.string().min(1),
|
|
41
|
+
read: z.unknown(),
|
|
42
|
+
normalize: z.unknown(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
/**
|
|
46
|
+
* Validates an adapter module against the frozen contract, failing
|
|
47
|
+
* closed with one issue per violated field.
|
|
48
|
+
*
|
|
49
|
+
* Args:
|
|
50
|
+
* value: The default export of an `.mjs` adapter module.
|
|
51
|
+
*
|
|
52
|
+
* Returns:
|
|
53
|
+
* WebhookEntityAdapterValidation: The typed adapter, or every issue found.
|
|
54
|
+
*/
|
|
55
|
+
export function validateWebhookEntityAdapter(value) {
|
|
56
|
+
const parsed = WebhookEntityAdapterSchema.safeParse(value);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
issues: parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const candidate = value;
|
|
64
|
+
const issues = [];
|
|
65
|
+
if (typeof candidate.read !== 'function') {
|
|
66
|
+
issues.push('read: must be a function (ctx, id) => Promise<unknown>');
|
|
67
|
+
}
|
|
68
|
+
if (typeof candidate.normalize !== 'function') {
|
|
69
|
+
issues.push('normalize: must be a function (body) => { entityId, fields }');
|
|
70
|
+
}
|
|
71
|
+
if (issues.length > 0)
|
|
72
|
+
return { ok: false, issues };
|
|
73
|
+
return { ok: true, adapter: value };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=adapter-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-schema.js","sourceRoot":"","sources":["../src/adapter-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2BxB;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC;KACxC,MAAM,CAAC;IACN,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACrC,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACjB,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE;CACvB,CAAC;KACD,MAAM,EAAE,CAAC;AAOZ;;;;;;;;;GASG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAc;IACzD,MAAM,MAAM,GAAG,0BAA0B,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC3D,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;SACxF,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,KAAsC,CAAC;IACzD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,SAAS,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QAC9C,MAAM,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACpD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAA6B,EAAE,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DiscoveryOutcome } from '@gate-forge/plugin-protocol';
|
|
2
|
+
/** Detected framework of a webhook endpoint. */
|
|
3
|
+
export type WebhookFramework = 'express' | 'fastify' | 'hono' | 'decorator';
|
|
4
|
+
/** Allowed signature algorithms the detector records. */
|
|
5
|
+
export type SignatureAlgorithm = 'hmac-sha256' | 'hmac-sha1' | 'none';
|
|
6
|
+
/** The detector's resource-attribute payload (deterministic, JSON-safe). */
|
|
7
|
+
export interface WebhookResourceAttributes {
|
|
8
|
+
/** Framework the resource was detected in. */
|
|
9
|
+
framework: WebhookFramework;
|
|
10
|
+
/** Stable path the endpoint is mounted at. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** HTTP methods the route accepts (sorted). */
|
|
13
|
+
httpMethods: string[];
|
|
14
|
+
/** HTTP header the signature is read from (default `x-signature`). */
|
|
15
|
+
signatureHeader: string;
|
|
16
|
+
/** HMAC variant the signature uses (`none` if no signature detected). */
|
|
17
|
+
signatureAlgorithm: SignatureAlgorithm;
|
|
18
|
+
/** Anti-replay window in milliseconds (default 300000 = 5 minutes). */
|
|
19
|
+
replayWindow: number;
|
|
20
|
+
/** Maximum accepted body size in bytes (default 1 MiB). */
|
|
21
|
+
maxBody: number;
|
|
22
|
+
/** Provider token extracted from the path or decorator argument. */
|
|
23
|
+
provider: string;
|
|
24
|
+
}
|
|
25
|
+
/** Detector options. */
|
|
26
|
+
export interface WebhookDetectorOptions {
|
|
27
|
+
/** Repo-root directory used to compute repo-relative paths. */
|
|
28
|
+
root?: string;
|
|
29
|
+
/** Default replay window override (ms). Defaults to 300000. */
|
|
30
|
+
defaultReplayWindow?: number;
|
|
31
|
+
/** Default max body override (bytes). Defaults to 1_048_576 (1 MiB). */
|
|
32
|
+
defaultMaxBody?: number;
|
|
33
|
+
}
|
|
34
|
+
/** The pinned in-process plugin contract. */
|
|
35
|
+
export interface WebhookDetector {
|
|
36
|
+
discover(paths: readonly string[]): DiscoveryOutcome;
|
|
37
|
+
}
|
|
38
|
+
/** The default exports for the pack: `discover(paths)`. */
|
|
39
|
+
export declare function discoverWebhooks(paths: readonly string[], options?: WebhookDetectorOptions): DiscoveryOutcome;
|
|
40
|
+
/** Build a discover-capable detector module. */
|
|
41
|
+
export declare function createWebhookDetector(options?: WebhookDetectorOptions): WebhookDetector;
|
|
42
|
+
//# sourceMappingURL=detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAAE,gBAAgB,EAAW,MAAM,6BAA6B,CAAC;AAG7E,gDAAgD;AAChD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC;AAE5E,yDAAyD;AACzD,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,WAAW,GAAG,MAAM,CAAC;AAEtE,4EAA4E;AAC5E,MAAM,WAAW,yBAAyB;IACxC,8CAA8C;IAC9C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAwB;AACxB,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAAC;CACtD;AAyOD,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,sBAA2B,GAAG,gBAAgB,CA2EjH;AAeD,gDAAgD;AAChD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAM3F"}
|
package/dist/detector.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook detector (pack-webhook).
|
|
3
|
+
*
|
|
4
|
+
* Pure TypeScript scan (no subprocess, no Python): walks `.ts`, `.tsx`,
|
|
5
|
+
* `.js`, `.mjs` files and detects webhook endpoint patterns across
|
|
6
|
+
* four surfaces:
|
|
7
|
+
*
|
|
8
|
+
* 1. Express — `app.post(...)` or `router.post(...)` whose first
|
|
9
|
+
* argument's path literal contains a webhook-y segment: `webhook`,
|
|
10
|
+
* `hook`, or `callback` (case-insensitive, dynamic `:id` segments
|
|
11
|
+
* allowed).
|
|
12
|
+
* 2. Fastify — same shape; the webhook path marker lives in the
|
|
13
|
+
* FIRST string-literal argument of `fastify.METHOD(...)`.
|
|
14
|
+
* 3. Hono — same shape; the webhook path marker lives in the
|
|
15
|
+
* FIRST string-literal argument of `app.METHOD(...)` from a file
|
|
16
|
+
* that imports `hono` (the receiver name `app` is otherwise
|
|
17
|
+
* identical to Express; we disambiguate by import context).
|
|
18
|
+
* 4. Decorator-based — `@webhook('stripe.payments')` or
|
|
19
|
+
* `@on('webhook.stripe.payments')` on a controller method.
|
|
20
|
+
*
|
|
21
|
+
* Each detected endpoint becomes a `webhook.endpoint` resource:
|
|
22
|
+
* {
|
|
23
|
+
* id: "webhook.<provider>.<endpoint>",
|
|
24
|
+
* kind: "webhook.endpoint",
|
|
25
|
+
* attributes: {
|
|
26
|
+
* signatureHeader: string,
|
|
27
|
+
* signatureAlgorithm: 'hmac-sha256' | 'hmac-sha1' | 'none',
|
|
28
|
+
* replayWindow: number (ms, default 300_000),
|
|
29
|
+
* maxBody: number (bytes),
|
|
30
|
+
* framework: 'express' | 'fastify' | 'hono' | 'decorator',
|
|
31
|
+
* path: string,
|
|
32
|
+
* httpMethods: string[],
|
|
33
|
+
* provider: string,
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* Findings emitted:
|
|
38
|
+
* - `DUPLICATE_RESOURCE_ID` — same id derived twice (fail-closed).
|
|
39
|
+
* - `PARSE_ERROR` — GF-19-style fallback when the scanner cannot
|
|
40
|
+
* read a file.
|
|
41
|
+
*
|
|
42
|
+
* Determinism: stable sort + derived ids only; no `Date.now()`, no
|
|
43
|
+
* `Math.random()`. Resource ids encode provider + endpoint token so
|
|
44
|
+
* multiple webhooks in one file never collide.
|
|
45
|
+
*/
|
|
46
|
+
import { readFileSync, statSync, readdirSync } from 'node:fs';
|
|
47
|
+
import { join, extname, relative, resolve } from 'node:path';
|
|
48
|
+
import { GATEFORGE_SCHEMA_VERSION } from '@gate-forge/core';
|
|
49
|
+
import { PACK_PLUGIN_ID, PACK_VERSION } from './version.js';
|
|
50
|
+
/** Default replay window if not otherwise configured: 5 minutes. */
|
|
51
|
+
const DEFAULT_REPLAY_WINDOW_MS = 300_000;
|
|
52
|
+
/** Default body cap if not otherwise configured: 1 MiB. */
|
|
53
|
+
const DEFAULT_MAX_BODY_BYTES = 1_048_576;
|
|
54
|
+
/** Default signature header if not otherwise configured. */
|
|
55
|
+
const DEFAULT_SIGNATURE_HEADER = 'x-signature';
|
|
56
|
+
/** Token regex used to detect webhook paths. */
|
|
57
|
+
const WEBHOOK_TOKEN = /webhook|hook|callback/i;
|
|
58
|
+
/** Skip list for the file walker. */
|
|
59
|
+
const SKIP_DIRS = { 'node_modules': true, 'dist': true, '.git': true };
|
|
60
|
+
/** Walks `paths` collecting every `.ts`/`.tsx`/`.js`/`.mjs` file. */
|
|
61
|
+
function collectFiles(paths) {
|
|
62
|
+
const out = new Set();
|
|
63
|
+
const visit = (abs) => {
|
|
64
|
+
let stat;
|
|
65
|
+
try {
|
|
66
|
+
stat = statSync(abs);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (stat.isFile()) {
|
|
72
|
+
const ext = extname(abs);
|
|
73
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.mjs') {
|
|
74
|
+
out.add(abs);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
const entries = readdirSync(abs, { withFileTypes: true });
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (SKIP_DIRS[entry.name] === true)
|
|
82
|
+
continue;
|
|
83
|
+
visit(join(abs, entry.name));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
for (const p of paths)
|
|
88
|
+
visit(resolve(p));
|
|
89
|
+
return [...out].sort();
|
|
90
|
+
}
|
|
91
|
+
/** Convert an absolute file path into a forward-slash, repo-relative path. */
|
|
92
|
+
function toRepoRelative(root, absFile) {
|
|
93
|
+
const rel = relative(root, absFile).replace(/\\/g, '/');
|
|
94
|
+
return rel.startsWith('/') ? rel.slice(1) : rel;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extract the provider token from a webhook path. Returns the first
|
|
98
|
+
* remaining segment after stripping the webhook marker tokens
|
|
99
|
+
* (`webhook`, `hook`, `callback`), lowercased and slug-ified.
|
|
100
|
+
*/
|
|
101
|
+
function providerFromPath(path) {
|
|
102
|
+
const cleaned = path.replace(/^\/+|\/+$/g, '');
|
|
103
|
+
const segments = cleaned.split('/').filter((s) => s.length > 0 && !s.startsWith(':') && !s.startsWith('{'));
|
|
104
|
+
const remaining = segments.filter((s) => !WEBHOOK_TOKEN.test(s));
|
|
105
|
+
const provider = remaining.length > 0 ? (remaining[0] ?? 'default') : (segments[0] ?? 'default');
|
|
106
|
+
return provider.replace(/[^a-zA-Z0-9.]+/g, '-').toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
/** Heuristic algorithm inference from a path / config string. */
|
|
109
|
+
function inferAlgorithm(hint) {
|
|
110
|
+
if (!hint)
|
|
111
|
+
return 'hmac-sha256';
|
|
112
|
+
const lower = hint.toLowerCase();
|
|
113
|
+
if (lower.includes('sha1'))
|
|
114
|
+
return 'hmac-sha1';
|
|
115
|
+
if (lower.includes('sha256'))
|
|
116
|
+
return 'hmac-sha256';
|
|
117
|
+
if (lower.includes('none') || lower.includes('unsigned'))
|
|
118
|
+
return 'none';
|
|
119
|
+
return 'hmac-sha256';
|
|
120
|
+
}
|
|
121
|
+
/** Signature-header inference from adjacent text. */
|
|
122
|
+
function inferSignatureHeader(text) {
|
|
123
|
+
const patterns = [
|
|
124
|
+
/signatureHeader\s*[:=]\s*['"]([^'"]+)['"]/,
|
|
125
|
+
/signature\s*[:=]\s*['"]([^'"]+)['"]/,
|
|
126
|
+
/headers\s*\[\s*['"]([^'"]+)['"]\s*\]/,
|
|
127
|
+
/req\.header\s*\(\s*['"]([^'"]+)['"]\s*\)/,
|
|
128
|
+
];
|
|
129
|
+
for (const re of patterns) {
|
|
130
|
+
const m = text.match(re);
|
|
131
|
+
if (m && m[1])
|
|
132
|
+
return m[1].toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
return DEFAULT_SIGNATURE_HEADER;
|
|
135
|
+
}
|
|
136
|
+
/** Locate the line+col of `index` in `text`. */
|
|
137
|
+
function lineColOf(text, index) {
|
|
138
|
+
const pre = text.slice(0, index);
|
|
139
|
+
const line = pre.split('\n').length;
|
|
140
|
+
const lastNewline = pre.lastIndexOf('\n');
|
|
141
|
+
const col = lastNewline < 0 ? index : index - lastNewline - 1;
|
|
142
|
+
return { line, col: Math.max(0, col) };
|
|
143
|
+
}
|
|
144
|
+
/** True when the file imports `hono`. Used to disambiguate `app.METHOD(...)`. */
|
|
145
|
+
function isHonoContext(text) {
|
|
146
|
+
return /from\s+['"]hono['"]|require\s*\(\s*['"]hono['"]\s*\)/.test(text);
|
|
147
|
+
}
|
|
148
|
+
/** Map a route-registration receiver name to its framework. */
|
|
149
|
+
function frameworkOfReceiver(receiver, fileText) {
|
|
150
|
+
if (receiver === 'fastify')
|
|
151
|
+
return 'fastify';
|
|
152
|
+
if (receiver === 'hono')
|
|
153
|
+
return 'hono';
|
|
154
|
+
if (receiver === 'app' && isHonoContext(fileText))
|
|
155
|
+
return 'hono';
|
|
156
|
+
return 'express';
|
|
157
|
+
}
|
|
158
|
+
/** Build the standard `webhook.endpoint` shape for one detected endpoint. */
|
|
159
|
+
function buildResource(d, id, file) {
|
|
160
|
+
const attributes = {
|
|
161
|
+
framework: d.framework,
|
|
162
|
+
path: d.path,
|
|
163
|
+
httpMethods: [...d.httpMethods].sort(),
|
|
164
|
+
signatureHeader: d.signatureHeader,
|
|
165
|
+
signatureAlgorithm: d.signatureAlgorithm,
|
|
166
|
+
replayWindow: d.replayWindow,
|
|
167
|
+
maxBody: d.maxBody,
|
|
168
|
+
provider: d.provider,
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
schemaVersion: GATEFORGE_SCHEMA_VERSION,
|
|
172
|
+
id,
|
|
173
|
+
kind: 'webhook.endpoint',
|
|
174
|
+
source: file,
|
|
175
|
+
location: { file, line: d.sourceLine, col: d.sourceCol },
|
|
176
|
+
detectorVersion: PACK_VERSION,
|
|
177
|
+
attributes,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Build a `Finding` with one location. */
|
|
181
|
+
function finding(code, detail, file, line, col) {
|
|
182
|
+
return {
|
|
183
|
+
code,
|
|
184
|
+
detail,
|
|
185
|
+
locations: [{ file, line, col }],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Scan for route registrations whose FIRST string-literal path is webhook-shaped. */
|
|
189
|
+
function scanRouteRegistries(text, file, options) {
|
|
190
|
+
const out = [];
|
|
191
|
+
const re = /(app|router|fastify|hono)\.(get|post|put|patch|delete|all)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
192
|
+
let m;
|
|
193
|
+
while ((m = re.exec(text)) !== null) {
|
|
194
|
+
const receiver = (m[1] ?? '').toLowerCase();
|
|
195
|
+
const method = (m[2] ?? '').toUpperCase();
|
|
196
|
+
const path = m[3] ?? '';
|
|
197
|
+
if (!WEBHOOK_TOKEN.test(path))
|
|
198
|
+
continue;
|
|
199
|
+
const tailStart = m.index;
|
|
200
|
+
const tailEnd = Math.min(text.length, tailStart + 800);
|
|
201
|
+
const tail = text.slice(tailStart, tailEnd);
|
|
202
|
+
const signatureHeader = inferSignatureHeader(tail);
|
|
203
|
+
const algorithm = inferAlgorithm(tail);
|
|
204
|
+
const framework = frameworkOfReceiver(receiver, text);
|
|
205
|
+
const { line, col } = lineColOf(text, m.index);
|
|
206
|
+
out.push({
|
|
207
|
+
framework,
|
|
208
|
+
path,
|
|
209
|
+
httpMethods: [method],
|
|
210
|
+
provider: providerFromPath(path),
|
|
211
|
+
signatureHeader,
|
|
212
|
+
signatureAlgorithm: algorithm,
|
|
213
|
+
replayWindow: options.defaultReplayWindow,
|
|
214
|
+
maxBody: options.defaultMaxBody,
|
|
215
|
+
sourceFile: file,
|
|
216
|
+
sourceLine: line,
|
|
217
|
+
sourceCol: col,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
/** Scan for `@webhook(...)` / `@on('webhook.x')` decorator-shaped endpoints. */
|
|
223
|
+
function scanDecorators(text, file, options) {
|
|
224
|
+
const out = [];
|
|
225
|
+
const re = /@(webhook|on)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
226
|
+
let m;
|
|
227
|
+
while ((m = re.exec(text)) !== null) {
|
|
228
|
+
const decoratorName = (m[1] ?? '').toLowerCase();
|
|
229
|
+
const literal = m[2] ?? '';
|
|
230
|
+
if (decoratorName === 'on' && !/^webhook[.:]/i.test(literal))
|
|
231
|
+
continue;
|
|
232
|
+
const providerRaw = literal.replace(/^webhook[.:]+/i, '');
|
|
233
|
+
const provider = providerRaw.replace(/[^a-zA-Z0-9.]+/g, '-').toLowerCase();
|
|
234
|
+
const tailStart = m.index;
|
|
235
|
+
const tailEnd = Math.min(text.length, tailStart + 600);
|
|
236
|
+
const tail = text.slice(tailStart, tailEnd);
|
|
237
|
+
const httpMatch = tail.match(/@(Get|Post|Put|Patch|Delete|All)\s*\(\s*['"]([^'"]+)['"]/i);
|
|
238
|
+
const method = httpMatch ? (httpMatch[1] ?? 'POST').toUpperCase() : 'POST';
|
|
239
|
+
const path = httpMatch ? (httpMatch[2] ?? '/webhook') : '/webhook';
|
|
240
|
+
const signatureHeader = inferSignatureHeader(tail);
|
|
241
|
+
const algorithm = inferAlgorithm(tail);
|
|
242
|
+
const { line, col } = lineColOf(text, m.index);
|
|
243
|
+
out.push({
|
|
244
|
+
framework: 'decorator',
|
|
245
|
+
path,
|
|
246
|
+
httpMethods: [method],
|
|
247
|
+
provider: provider === '' ? 'default' : provider,
|
|
248
|
+
signatureHeader,
|
|
249
|
+
signatureAlgorithm: algorithm,
|
|
250
|
+
replayWindow: options.defaultReplayWindow,
|
|
251
|
+
maxBody: options.defaultMaxBody,
|
|
252
|
+
sourceFile: file,
|
|
253
|
+
sourceLine: line,
|
|
254
|
+
sourceCol: col,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
/** Build a stable resource id from a detected webhook. */
|
|
260
|
+
function webhookId(d) {
|
|
261
|
+
const cleaned = d.path.replace(/^\/+|\/+$/g, '');
|
|
262
|
+
const segments = cleaned.split('/').filter((s) => s.length > 0 && !s.startsWith(':') && !s.startsWith('{'));
|
|
263
|
+
const endpoint = segments.length > 0 ? (segments[segments.length - 1] ?? 'default') : 'default';
|
|
264
|
+
const provider = d.provider === '' ? 'default' : d.provider;
|
|
265
|
+
const safeEndpoint = endpoint.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase() || 'default';
|
|
266
|
+
return `webhook.${provider}.${safeEndpoint}`;
|
|
267
|
+
}
|
|
268
|
+
/** The default exports for the pack: `discover(paths)`. */
|
|
269
|
+
export function discoverWebhooks(paths, options = {}) {
|
|
270
|
+
const root = options.root ?? process.cwd();
|
|
271
|
+
const files = collectFiles(paths);
|
|
272
|
+
const opt = {
|
|
273
|
+
defaultReplayWindow: options.defaultReplayWindow ?? DEFAULT_REPLAY_WINDOW_MS,
|
|
274
|
+
defaultMaxBody: options.defaultMaxBody ?? DEFAULT_MAX_BODY_BYTES,
|
|
275
|
+
};
|
|
276
|
+
const detected = [];
|
|
277
|
+
const findings = [];
|
|
278
|
+
const scanned = [];
|
|
279
|
+
for (const abs of files) {
|
|
280
|
+
const file = toRepoRelative(root, abs);
|
|
281
|
+
let text;
|
|
282
|
+
try {
|
|
283
|
+
text = readFileSync(abs, 'utf8');
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
findings.push(finding('PARSE_ERROR', `unable to read ${file}`, file, 1, 0));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
scanned.push(file);
|
|
290
|
+
detected.push(...scanRouteRegistries(text, file, opt), ...scanDecorators(text, file, opt));
|
|
291
|
+
}
|
|
292
|
+
// Build resources with deterministic ids; flag duplicates.
|
|
293
|
+
const seen = new Map();
|
|
294
|
+
const resources = [];
|
|
295
|
+
for (const d of detected) {
|
|
296
|
+
const id = webhookId(d);
|
|
297
|
+
const existing = seen.get(id);
|
|
298
|
+
if (existing !== undefined) {
|
|
299
|
+
findings.push(finding('DUPLICATE_RESOURCE_ID', `duplicate webhook id ${id} (also at ${existing})`, d.sourceFile, d.sourceLine, d.sourceCol));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
seen.set(id, d.sourceFile);
|
|
303
|
+
resources.push(buildResource(d, id, d.sourceFile));
|
|
304
|
+
}
|
|
305
|
+
resources.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
306
|
+
findings.sort((a, b) => {
|
|
307
|
+
if (a.code !== b.code)
|
|
308
|
+
return a.code < b.code ? -1 : 1;
|
|
309
|
+
if (a.detail !== b.detail)
|
|
310
|
+
return a.detail < b.detail ? -1 : 1;
|
|
311
|
+
return 0;
|
|
312
|
+
});
|
|
313
|
+
// Phase-4 linkage (plan phase 4, ADR 0003 D1/D2): a webhook endpoint is
|
|
314
|
+
// externally reachable by definition — one code-positive `exposure`
|
|
315
|
+
// signal per endpoint, targeted at the path-derived resource name so
|
|
316
|
+
// the core classifier can converge it with a discovered table. An
|
|
317
|
+
// underivable name emits no signal (nothing is claimed). No lifecycle
|
|
318
|
+
// or negative claims exist in this pack: receiving events proves
|
|
319
|
+
// reachability, nothing more.
|
|
320
|
+
const classificationSignals = resources.flatMap((resource) => {
|
|
321
|
+
const attributes = resource.attributes;
|
|
322
|
+
const target = lastPathName(attributes.path);
|
|
323
|
+
if (target === null)
|
|
324
|
+
return [];
|
|
325
|
+
return [{
|
|
326
|
+
schemaVersion: 1,
|
|
327
|
+
target: { resourceName: target },
|
|
328
|
+
dimension: 'exposure',
|
|
329
|
+
assertion: 'webhook',
|
|
330
|
+
basis: 'code-positive',
|
|
331
|
+
source: PACK_PLUGIN_ID,
|
|
332
|
+
location: resource.location,
|
|
333
|
+
detector: { id: PACK_PLUGIN_ID, version: PACK_VERSION },
|
|
334
|
+
}];
|
|
335
|
+
});
|
|
336
|
+
return { resources, unresolved: [], findings, classificationSignals, scannedPaths: scanned.sort() };
|
|
337
|
+
}
|
|
338
|
+
/** Last non-parameter path segment, lower-cased; null when underivable. */
|
|
339
|
+
function lastPathName(rawPath) {
|
|
340
|
+
const segments = rawPath.split('/').filter((segment) => segment.length > 0);
|
|
341
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
342
|
+
const segment = segments[i];
|
|
343
|
+
if (segment === undefined)
|
|
344
|
+
continue;
|
|
345
|
+
if (segment.startsWith(':') || segment.startsWith('{') || segment.startsWith('*'))
|
|
346
|
+
continue;
|
|
347
|
+
if (/^\d+$/.test(segment))
|
|
348
|
+
continue;
|
|
349
|
+
return segment.toLowerCase();
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
/** Build a discover-capable detector module. */
|
|
354
|
+
export function createWebhookDetector(options = {}) {
|
|
355
|
+
return {
|
|
356
|
+
discover(paths) {
|
|
357
|
+
return discoverWebhooks(paths, options);
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
//# sourceMappingURL=detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { PACK_PLUGIN_ID, PACK_VERSION } from './version.js';
|
|
2
|
+
export { createWebhookDetector, discoverWebhooks, type WebhookDetector, type WebhookDetectorOptions, type WebhookFramework, type WebhookResourceAttributes, type SignatureAlgorithm, } from './detector.js';
|
|
3
|
+
export { WEBHOOK_OBLIGATION_CONTRACTS, obligationId, type WebhookObligationContract, } from './obligations.js';
|
|
4
|
+
export { WebhookEntityAdapterSchema, validateWebhookEntityAdapter, type WebhookEntityAdapter, type WebhookEntityAdapterValidation, type EntityAdapterContext, type NormalizedEntity, } from './adapter-schema.js';
|
|
5
|
+
/** The default CLI in-process plugin module: `{ discover(paths) }`. */
|
|
6
|
+
declare const _default: import("./detector.js").WebhookDetector;
|
|
7
|
+
export default _default;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,4BAA4B,EAC5B,YAAY,EACZ,KAAK,yBAAyB,GAC/B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,KAAK,oBAAoB,EACzB,KAAK,8BAA8B,EACnC,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,uEAAuE;;AACvE,wBAAuC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gate-forge/pack-webhook — Webhook contract pack.
|
|
3
|
+
*
|
|
4
|
+
* Pure TypeScript detector that walks `.ts`/`.tsx`/`.js`/`.mjs` sources
|
|
5
|
+
* and discovers webhook endpoint patterns across four surfaces:
|
|
6
|
+
*
|
|
7
|
+
* - Express (route registration with webhook-shaped path)
|
|
8
|
+
* - Fastify (route registration with webhook-shaped path)
|
|
9
|
+
* - Hono (route registration with webhook-shaped path)
|
|
10
|
+
* - Decorator (NestJS-style `@webhook(...)` or `@on('webhook.x')`)
|
|
11
|
+
*
|
|
12
|
+
* Every webhook endpoint becomes a `webhook.endpoint` resource with a
|
|
13
|
+
* stable id (`webhook.<provider>.<endpoint>`) and typed attributes
|
|
14
|
+
* (`signatureHeader`, `signatureAlgorithm`, `replayWindow`, `maxBody`,
|
|
15
|
+
* `framework`, `path`, `httpMethods[]`, `provider`). Duplicate ids
|
|
16
|
+
* surface as `DUPLICATE_RESOURCE_ID` findings rather than silently
|
|
17
|
+
* overwriting; signature-shaped ambiguities emit `AMBIGUOUS_WEBHOOK`.
|
|
18
|
+
*
|
|
19
|
+
* The pack ships an entity-adapter schema (mirror of
|
|
20
|
+
* `@gate-forge/pack-sqlalchemy`'s) so the in-memory delivery log can be
|
|
21
|
+
* witnessed GET-only for the `webhook:replay-idempotent` and
|
|
22
|
+
* `webhook:retry-bounded` contracts.
|
|
23
|
+
*
|
|
24
|
+
* Default export is the CLI in-process plugin contract
|
|
25
|
+
* (`discover(paths)`); see README.md for setup + the example server
|
|
26
|
+
* (`example/webhook/server.js`, port 3003).
|
|
27
|
+
*/
|
|
28
|
+
import { createWebhookDetector } from './detector.js';
|
|
29
|
+
export { PACK_PLUGIN_ID, PACK_VERSION } from './version.js';
|
|
30
|
+
export { createWebhookDetector, discoverWebhooks, } from './detector.js';
|
|
31
|
+
export { WEBHOOK_OBLIGATION_CONTRACTS, obligationId, } from './obligations.js';
|
|
32
|
+
export { WebhookEntityAdapterSchema, validateWebhookEntityAdapter, } from './adapter-schema.js';
|
|
33
|
+
/** The default CLI in-process plugin module: `{ discover(paths) }`. */
|
|
34
|
+
export default createWebhookDetector();
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,EACL,qBAAqB,EACrB,gBAAgB,GAMjB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,4BAA4B,EAC5B,YAAY,GAEb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,GAK7B,MAAM,qBAAqB,CAAC;AAE7B,uEAAuE;AACvE,eAAe,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The webhook pack obligation vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Every obligation id is `<resourceId>:<contract>` where `<resourceId>`
|
|
5
|
+
* is the stable id the detector emits for one webhook endpoint
|
|
6
|
+
* (e.g. `webhook.stripe.payments`) and `<contract>` is one of the
|
|
7
|
+
* five names below.
|
|
8
|
+
*
|
|
9
|
+
* Contract grammar: `webhook:<verb>` (no interior colons). Trailing
|
|
10
|
+
* colons are rejected by `ContractNameSchema` upstream.
|
|
11
|
+
*
|
|
12
|
+
* Vocabulary rationale (per the pack brief):
|
|
13
|
+
*
|
|
14
|
+
* - `webhook:signature-accepted` — A request carrying a valid signature
|
|
15
|
+
* (HMAC-SHA256 over the canonical body, matched against the
|
|
16
|
+
* configured header) is accepted (2xx) and reaches the handler.
|
|
17
|
+
* Verified by issuing the request with a freshly-computed signature;
|
|
18
|
+
* the handler MUST produce its declared success status.
|
|
19
|
+
*
|
|
20
|
+
* - `webhook:signature-rejected` — A request whose signature does NOT
|
|
21
|
+
* match the canonical body / header / secret is rejected with 401
|
|
22
|
+
* and NEVER reaches the handler. Verified by mutating one byte of the
|
|
23
|
+
* payload while leaving the signature untouched (or vice-versa).
|
|
24
|
+
*
|
|
25
|
+
* - `webhook:malformed-rejected` — A request whose body is not valid
|
|
26
|
+
* JSON (or whose Content-Type is wrong / payload exceeds `maxBody`)
|
|
27
|
+
* is rejected with 400 and never reaches the signature verifier.
|
|
28
|
+
* Verified by sending a malformed JSON body.
|
|
29
|
+
*
|
|
30
|
+
* - `webhook:replay-idempotent` — When the same `event_id` arrives more
|
|
31
|
+
* than once within `attributes.replayWindow` ms, only the FIRST
|
|
32
|
+
* delivery produces a side effect; subsequent deliveries are deduped.
|
|
33
|
+
* Verified by issuing the same event_id twice with valid signatures
|
|
34
|
+
* and confirming the in-memory log has exactly one side-effect entry.
|
|
35
|
+
*
|
|
36
|
+
* - `webhook:retry-bounded` — The server enforces a hard ceiling on the
|
|
37
|
+
* number of retries it will accept per `event_id` (default 3). The
|
|
38
|
+
* server responds 429 once the ceiling is exceeded. Verified by
|
|
39
|
+
* firing the same event repeatedly with the `X-Webhook-Attempt`
|
|
40
|
+
* header and confirming the response transitions 2xx → 2xx → 2xx →
|
|
41
|
+
* 429 at attempt 4.
|
|
42
|
+
*/
|
|
43
|
+
export declare const WEBHOOK_OBLIGATION_CONTRACTS: readonly ["webhook:signature-accepted", "webhook:signature-rejected", "webhook:malformed-rejected", "webhook:replay-idempotent", "webhook:retry-bounded"];
|
|
44
|
+
/** One webhook obligation contract name. */
|
|
45
|
+
export type WebhookObligationContract = (typeof WEBHOOK_OBLIGATION_CONTRACTS)[number];
|
|
46
|
+
/**
|
|
47
|
+
* Builds the canonical `<resourceId>:<contract>` obligation id for one
|
|
48
|
+
* webhook endpoint. Stable across runs (no clock, no randomness).
|
|
49
|
+
*
|
|
50
|
+
* Args:
|
|
51
|
+
* resourceId: The detector-emitted resource id (`webhook.<area>.<endpoint>`).
|
|
52
|
+
* contract: One of {@link WEBHOOK_OBLIGATION_CONTRACTS}.
|
|
53
|
+
*
|
|
54
|
+
* Returns:
|
|
55
|
+
* string: The `<resourceId>:<contract>` obligation id.
|
|
56
|
+
*/
|
|
57
|
+
export declare function obligationId(resourceId: string, contract: WebhookObligationContract): string;
|
|
58
|
+
//# sourceMappingURL=obligations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obligations.d.ts","sourceRoot":"","sources":["../src/obligations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,4BAA4B,2JAM/B,CAAC;AAEX,4CAA4C;AAC5C,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,yBAAyB,GAAG,MAAM,CAE5F"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The webhook pack obligation vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Every obligation id is `<resourceId>:<contract>` where `<resourceId>`
|
|
5
|
+
* is the stable id the detector emits for one webhook endpoint
|
|
6
|
+
* (e.g. `webhook.stripe.payments`) and `<contract>` is one of the
|
|
7
|
+
* five names below.
|
|
8
|
+
*
|
|
9
|
+
* Contract grammar: `webhook:<verb>` (no interior colons). Trailing
|
|
10
|
+
* colons are rejected by `ContractNameSchema` upstream.
|
|
11
|
+
*
|
|
12
|
+
* Vocabulary rationale (per the pack brief):
|
|
13
|
+
*
|
|
14
|
+
* - `webhook:signature-accepted` — A request carrying a valid signature
|
|
15
|
+
* (HMAC-SHA256 over the canonical body, matched against the
|
|
16
|
+
* configured header) is accepted (2xx) and reaches the handler.
|
|
17
|
+
* Verified by issuing the request with a freshly-computed signature;
|
|
18
|
+
* the handler MUST produce its declared success status.
|
|
19
|
+
*
|
|
20
|
+
* - `webhook:signature-rejected` — A request whose signature does NOT
|
|
21
|
+
* match the canonical body / header / secret is rejected with 401
|
|
22
|
+
* and NEVER reaches the handler. Verified by mutating one byte of the
|
|
23
|
+
* payload while leaving the signature untouched (or vice-versa).
|
|
24
|
+
*
|
|
25
|
+
* - `webhook:malformed-rejected` — A request whose body is not valid
|
|
26
|
+
* JSON (or whose Content-Type is wrong / payload exceeds `maxBody`)
|
|
27
|
+
* is rejected with 400 and never reaches the signature verifier.
|
|
28
|
+
* Verified by sending a malformed JSON body.
|
|
29
|
+
*
|
|
30
|
+
* - `webhook:replay-idempotent` — When the same `event_id` arrives more
|
|
31
|
+
* than once within `attributes.replayWindow` ms, only the FIRST
|
|
32
|
+
* delivery produces a side effect; subsequent deliveries are deduped.
|
|
33
|
+
* Verified by issuing the same event_id twice with valid signatures
|
|
34
|
+
* and confirming the in-memory log has exactly one side-effect entry.
|
|
35
|
+
*
|
|
36
|
+
* - `webhook:retry-bounded` — The server enforces a hard ceiling on the
|
|
37
|
+
* number of retries it will accept per `event_id` (default 3). The
|
|
38
|
+
* server responds 429 once the ceiling is exceeded. Verified by
|
|
39
|
+
* firing the same event repeatedly with the `X-Webhook-Attempt`
|
|
40
|
+
* header and confirming the response transitions 2xx → 2xx → 2xx →
|
|
41
|
+
* 429 at attempt 4.
|
|
42
|
+
*/
|
|
43
|
+
export const WEBHOOK_OBLIGATION_CONTRACTS = [
|
|
44
|
+
'webhook:signature-accepted',
|
|
45
|
+
'webhook:signature-rejected',
|
|
46
|
+
'webhook:malformed-rejected',
|
|
47
|
+
'webhook:replay-idempotent',
|
|
48
|
+
'webhook:retry-bounded',
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Builds the canonical `<resourceId>:<contract>` obligation id for one
|
|
52
|
+
* webhook endpoint. Stable across runs (no clock, no randomness).
|
|
53
|
+
*
|
|
54
|
+
* Args:
|
|
55
|
+
* resourceId: The detector-emitted resource id (`webhook.<area>.<endpoint>`).
|
|
56
|
+
* contract: One of {@link WEBHOOK_OBLIGATION_CONTRACTS}.
|
|
57
|
+
*
|
|
58
|
+
* Returns:
|
|
59
|
+
* string: The `<resourceId>:<contract>` obligation id.
|
|
60
|
+
*/
|
|
61
|
+
export function obligationId(resourceId, contract) {
|
|
62
|
+
return `${resourceId}:${contract}`;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=obligations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obligations.js","sourceRoot":"","sources":["../src/obligations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,4BAA4B;IAC5B,4BAA4B;IAC5B,4BAA4B;IAC5B,2BAA2B;IAC3B,uBAAuB;CACf,CAAC;AAKX;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,UAAkB,EAAE,QAAmC;IAClF,OAAO,GAAG,UAAU,IAAI,QAAQ,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack identity constants. The GPP/3 handshake pins these values: the
|
|
3
|
+
* `.gateforge.yml` plugin entry MUST declare the same `id` and `version`
|
|
4
|
+
* (see the pack README).
|
|
5
|
+
*/
|
|
6
|
+
/** Plugin id every webhook pack contribution is pinned to. */
|
|
7
|
+
export declare const PACK_PLUGIN_ID = "gateforge.pack-webhook";
|
|
8
|
+
/** Detector/pack version. */
|
|
9
|
+
export declare const PACK_VERSION = "0.1.0";
|
|
10
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8DAA8D;AAC9D,eAAO,MAAM,cAAc,2BAA2B,CAAC;AAEvD,6BAA6B;AAC7B,eAAO,MAAM,YAAY,UAAU,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack identity constants. The GPP/3 handshake pins these values: the
|
|
3
|
+
* `.gateforge.yml` plugin entry MUST declare the same `id` and `version`
|
|
4
|
+
* (see the pack README).
|
|
5
|
+
*/
|
|
6
|
+
/** Plugin id every webhook pack contribution is pinned to. */
|
|
7
|
+
export const PACK_PLUGIN_ID = 'gateforge.pack-webhook';
|
|
8
|
+
/** Detector/pack version. */
|
|
9
|
+
export const PACK_VERSION = '0.1.0';
|
|
10
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEvD,6BAA6B;AAC7B,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gate-forge/pack-webhook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Webhook contract pack: TS-only detector for webhook endpoint patterns (Express router.post('/webhook'|'/hook'|'/callback'), Fastify/Hono equivalents, @webhook/@on decorator) emitting webhook.endpoint resources with signature/replay/retry attributes; loopback example server with HMAC-SHA256 verification, timestamp anti-replay, in-memory delivery dedup, bounded retries to prove five obligation contracts end-to-end.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"README.md",
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@gate-forge/core": "^0.1.0",
|
|
27
|
+
"@gate-forge/plugin-protocol": "^0.1.0",
|
|
28
|
+
"zod": "^4.1.5"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/umiddey/gateforge.git",
|
|
36
|
+
"directory": "packages/pack-webhook"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/umiddey/gateforge/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/umiddey/gateforge#readme"
|
|
42
|
+
}
|