@crowdsource.you/core 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +571 -0
- package/dist/cases.d.ts +66 -0
- package/dist/cases.d.ts.map +1 -0
- package/dist/cases.js +79 -0
- package/dist/cases.js.map +1 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +125 -0
- package/dist/client.js.map +1 -0
- package/dist/communityNotes.d.ts +49 -0
- package/dist/communityNotes.d.ts.map +1 -0
- package/dist/communityNotes.js +104 -0
- package/dist/communityNotes.js.map +1 -0
- package/dist/credential.d.ts +58 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +71 -0
- package/dist/credential.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +82 -0
- package/dist/defaults.js.map +1 -0
- package/dist/digest.d.ts +44 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +79 -0
- package/dist/digest.js.map +1 -0
- package/dist/envelope.d.ts +143 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +258 -0
- package/dist/envelope.js.map +1 -0
- package/dist/errors.d.ts +90 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +151 -0
- package/dist/errors.js.map +1 -0
- package/dist/esm/cases.js +74 -0
- package/dist/esm/cases.js.map +1 -0
- package/dist/esm/client.js +121 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/communityNotes.js +100 -0
- package/dist/esm/communityNotes.js.map +1 -0
- package/dist/esm/credential.js +67 -0
- package/dist/esm/credential.js.map +1 -0
- package/dist/esm/defaults.js +77 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/digest.js +74 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/envelope.js +252 -0
- package/dist/esm/envelope.js.map +1 -0
- package/dist/esm/errors.js +140 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/express/index.js +29 -0
- package/dist/esm/express/index.js.map +1 -0
- package/dist/esm/express/middleware.js +293 -0
- package/dist/esm/express/middleware.js.map +1 -0
- package/dist/esm/express/store.js +69 -0
- package/dist/esm/express/store.js.map +1 -0
- package/dist/esm/express/verify.js +133 -0
- package/dist/esm/express/verify.js.map +1 -0
- package/dist/esm/index.js +57 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/outbox/client.js +43 -0
- package/dist/esm/outbox/client.js.map +1 -0
- package/dist/esm/outbox/decision.js +153 -0
- package/dist/esm/outbox/decision.js.map +1 -0
- package/dist/esm/outbox/delivery.js +129 -0
- package/dist/esm/outbox/delivery.js.map +1 -0
- package/dist/esm/outbox/enforcement/executor.js +219 -0
- package/dist/esm/outbox/enforcement/executor.js.map +1 -0
- package/dist/esm/outbox/enforcement/planner.js +326 -0
- package/dist/esm/outbox/enforcement/planner.js.map +1 -0
- package/dist/esm/outbox/evidence.js +173 -0
- package/dist/esm/outbox/evidence.js.map +1 -0
- package/dist/esm/outbox/inbound.js +87 -0
- package/dist/esm/outbox/inbound.js.map +1 -0
- package/dist/esm/outbox/index.js +71 -0
- package/dist/esm/outbox/index.js.map +1 -0
- package/dist/esm/outbox/intake.js +140 -0
- package/dist/esm/outbox/intake.js.map +1 -0
- package/dist/esm/outbox/integration.js +116 -0
- package/dist/esm/outbox/integration.js.map +1 -0
- package/dist/esm/outbox/outbox/dispatcher.js +114 -0
- package/dist/esm/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/esm/outbox/outbox/service.js +271 -0
- package/dist/esm/outbox/outbox/service.js.map +1 -0
- package/dist/esm/outbox/postgres/index.js +41 -0
- package/dist/esm/outbox/postgres/index.js.map +1 -0
- package/dist/esm/outbox/postgres/registries.js +155 -0
- package/dist/esm/outbox/postgres/registries.js.map +1 -0
- package/dist/esm/outbox/postgres/reportColumns.js +191 -0
- package/dist/esm/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/esm/outbox/postgres/store/enforcement.js +158 -0
- package/dist/esm/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/esm/outbox/postgres/store/events.js +101 -0
- package/dist/esm/outbox/postgres/store/events.js.map +1 -0
- package/dist/esm/outbox/postgres/store/index.js +59 -0
- package/dist/esm/outbox/postgres/store/index.js.map +1 -0
- package/dist/esm/outbox/postgres/store/outbox.js +207 -0
- package/dist/esm/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/esm/outbox/postgres/store/reports.js +231 -0
- package/dist/esm/outbox/postgres/store/reports.js.map +1 -0
- package/dist/esm/outbox/postgres/store/transaction.js +29 -0
- package/dist/esm/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/esm/outbox/postgres/tables.js +270 -0
- package/dist/esm/outbox/postgres/tables.js.map +1 -0
- package/dist/esm/outbox/reconciliation.js +157 -0
- package/dist/esm/outbox/reconciliation.js.map +1 -0
- package/dist/esm/outbox/reportStatus.js +22 -0
- package/dist/esm/outbox/reportStatus.js.map +1 -0
- package/dist/esm/outbox/retention.js +33 -0
- package/dist/esm/outbox/retention.js.map +1 -0
- package/dist/esm/outbox/store/types.js +30 -0
- package/dist/esm/outbox/store/types.js.map +1 -0
- package/dist/esm/outbox/types.js +23 -0
- package/dist/esm/outbox/types.js.map +1 -0
- package/dist/esm/outbox/webhook.js +145 -0
- package/dist/esm/outbox/webhook.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/reports.js +74 -0
- package/dist/esm/reports.js.map +1 -0
- package/dist/esm/testing/fixtures.js +132 -0
- package/dist/esm/testing/fixtures.js.map +1 -0
- package/dist/esm/testing/index.js +33 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/testing/sandbox.js +337 -0
- package/dist/esm/testing/sandbox.js.map +1 -0
- package/dist/esm/testing/webhook-simulator.js +93 -0
- package/dist/esm/testing/webhook-simulator.js.map +1 -0
- package/dist/esm/transport.js +177 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/webhookEndpoints.js +139 -0
- package/dist/esm/webhookEndpoints.js.map +1 -0
- package/dist/express/index.d.ts +32 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +40 -0
- package/dist/express/index.js.map +1 -0
- package/dist/express/middleware.d.ts +97 -0
- package/dist/express/middleware.d.ts.map +1 -0
- package/dist/express/middleware.js +298 -0
- package/dist/express/middleware.js.map +1 -0
- package/dist/express/store.d.ts +45 -0
- package/dist/express/store.d.ts.map +1 -0
- package/dist/express/store.js +72 -0
- package/dist/express/store.js.map +1 -0
- package/dist/express/verify.d.ts +86 -0
- package/dist/express/verify.d.ts.map +1 -0
- package/dist/express/verify.js +138 -0
- package/dist/express/verify.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox/client.d.ts +35 -0
- package/dist/outbox/client.d.ts.map +1 -0
- package/dist/outbox/client.js +46 -0
- package/dist/outbox/client.js.map +1 -0
- package/dist/outbox/decision.d.ts +36 -0
- package/dist/outbox/decision.d.ts.map +1 -0
- package/dist/outbox/decision.js +159 -0
- package/dist/outbox/decision.js.map +1 -0
- package/dist/outbox/delivery.d.ts +50 -0
- package/dist/outbox/delivery.d.ts.map +1 -0
- package/dist/outbox/delivery.js +135 -0
- package/dist/outbox/delivery.js.map +1 -0
- package/dist/outbox/enforcement/executor.d.ts +26 -0
- package/dist/outbox/enforcement/executor.d.ts.map +1 -0
- package/dist/outbox/enforcement/executor.js +222 -0
- package/dist/outbox/enforcement/executor.js.map +1 -0
- package/dist/outbox/enforcement/planner.d.ts +50 -0
- package/dist/outbox/enforcement/planner.d.ts.map +1 -0
- package/dist/outbox/enforcement/planner.js +333 -0
- package/dist/outbox/enforcement/planner.js.map +1 -0
- package/dist/outbox/evidence.d.ts +114 -0
- package/dist/outbox/evidence.d.ts.map +1 -0
- package/dist/outbox/evidence.js +181 -0
- package/dist/outbox/evidence.js.map +1 -0
- package/dist/outbox/inbound.d.ts +78 -0
- package/dist/outbox/inbound.d.ts.map +1 -0
- package/dist/outbox/inbound.js +91 -0
- package/dist/outbox/inbound.js.map +1 -0
- package/dist/outbox/index.d.ts +79 -0
- package/dist/outbox/index.d.ts.map +1 -0
- package/dist/outbox/index.js +98 -0
- package/dist/outbox/index.js.map +1 -0
- package/dist/outbox/intake.d.ts +62 -0
- package/dist/outbox/intake.d.ts.map +1 -0
- package/dist/outbox/intake.js +145 -0
- package/dist/outbox/intake.js.map +1 -0
- package/dist/outbox/integration.d.ts +62 -0
- package/dist/outbox/integration.d.ts.map +1 -0
- package/dist/outbox/integration.js +119 -0
- package/dist/outbox/integration.js.map +1 -0
- package/dist/outbox/outbox/dispatcher.d.ts +26 -0
- package/dist/outbox/outbox/dispatcher.d.ts.map +1 -0
- package/dist/outbox/outbox/dispatcher.js +119 -0
- package/dist/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/outbox/outbox/service.d.ts +124 -0
- package/dist/outbox/outbox/service.d.ts.map +1 -0
- package/dist/outbox/outbox/service.js +279 -0
- package/dist/outbox/outbox/service.js.map +1 -0
- package/dist/outbox/postgres/index.d.ts +44 -0
- package/dist/outbox/postgres/index.d.ts.map +1 -0
- package/dist/outbox/postgres/index.js +51 -0
- package/dist/outbox/postgres/index.js.map +1 -0
- package/dist/outbox/postgres/registries.d.ts +69 -0
- package/dist/outbox/postgres/registries.d.ts.map +1 -0
- package/dist/outbox/postgres/registries.js +159 -0
- package/dist/outbox/postgres/registries.js.map +1 -0
- package/dist/outbox/postgres/reportColumns.d.ts +154 -0
- package/dist/outbox/postgres/reportColumns.d.ts.map +1 -0
- package/dist/outbox/postgres/reportColumns.js +195 -0
- package/dist/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/outbox/postgres/store/enforcement.d.ts +33 -0
- package/dist/outbox/postgres/store/enforcement.d.ts.map +1 -0
- package/dist/outbox/postgres/store/enforcement.js +161 -0
- package/dist/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/outbox/postgres/store/events.d.ts +46 -0
- package/dist/outbox/postgres/store/events.d.ts.map +1 -0
- package/dist/outbox/postgres/store/events.js +104 -0
- package/dist/outbox/postgres/store/events.js.map +1 -0
- package/dist/outbox/postgres/store/index.d.ts +27 -0
- package/dist/outbox/postgres/store/index.d.ts.map +1 -0
- package/dist/outbox/postgres/store/index.js +62 -0
- package/dist/outbox/postgres/store/index.js.map +1 -0
- package/dist/outbox/postgres/store/outbox.d.ts +8 -0
- package/dist/outbox/postgres/store/outbox.d.ts.map +1 -0
- package/dist/outbox/postgres/store/outbox.js +210 -0
- package/dist/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/outbox/postgres/store/reports.d.ts +69 -0
- package/dist/outbox/postgres/store/reports.d.ts.map +1 -0
- package/dist/outbox/postgres/store/reports.js +234 -0
- package/dist/outbox/postgres/store/reports.js.map +1 -0
- package/dist/outbox/postgres/store/transaction.d.ts +49 -0
- package/dist/outbox/postgres/store/transaction.d.ts.map +1 -0
- package/dist/outbox/postgres/store/transaction.js +32 -0
- package/dist/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/outbox/postgres/tables.d.ts +737 -0
- package/dist/outbox/postgres/tables.d.ts.map +1 -0
- package/dist/outbox/postgres/tables.js +273 -0
- package/dist/outbox/postgres/tables.js.map +1 -0
- package/dist/outbox/reconciliation.d.ts +49 -0
- package/dist/outbox/reconciliation.d.ts.map +1 -0
- package/dist/outbox/reconciliation.js +162 -0
- package/dist/outbox/reconciliation.js.map +1 -0
- package/dist/outbox/reportStatus.d.ts +12 -0
- package/dist/outbox/reportStatus.d.ts.map +1 -0
- package/dist/outbox/reportStatus.js +25 -0
- package/dist/outbox/reportStatus.js.map +1 -0
- package/dist/outbox/retention.d.ts +33 -0
- package/dist/outbox/retention.d.ts.map +1 -0
- package/dist/outbox/retention.js +36 -0
- package/dist/outbox/retention.js.map +1 -0
- package/dist/outbox/store/types.d.ts +466 -0
- package/dist/outbox/store/types.d.ts.map +1 -0
- package/dist/outbox/store/types.js +31 -0
- package/dist/outbox/store/types.js.map +1 -0
- package/dist/outbox/types.d.ts +660 -0
- package/dist/outbox/types.d.ts.map +1 -0
- package/dist/outbox/types.js +24 -0
- package/dist/outbox/types.js.map +1 -0
- package/dist/outbox/webhook.d.ts +15 -0
- package/dist/outbox/webhook.d.ts.map +1 -0
- package/dist/outbox/webhook.js +148 -0
- package/dist/outbox/webhook.js.map +1 -0
- package/dist/reports.d.ts +73 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +78 -0
- package/dist/reports.js.map +1 -0
- package/dist/testing/fixtures.d.ts +62 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +137 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +43 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/sandbox.d.ts +109 -0
- package/dist/testing/sandbox.d.ts.map +1 -0
- package/dist/testing/sandbox.js +342 -0
- package/dist/testing/sandbox.js.map +1 -0
- package/dist/testing/webhook-simulator.d.ts +78 -0
- package/dist/testing/webhook-simulator.d.ts.map +1 -0
- package/dist/testing/webhook-simulator.js +98 -0
- package/dist/testing/webhook-simulator.js.map +1 -0
- package/dist/transport.d.ts +59 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +181 -0
- package/dist/transport.js.map +1 -0
- package/dist/webhookEndpoints.d.ts +147 -0
- package/dist/webhookEndpoints.d.ts.map +1 -0
- package/dist/webhookEndpoints.js +143 -0
- package/dist/webhookEndpoints.js.map +1 -0
- package/package.json +127 -0
- package/src/cases.ts +119 -0
- package/src/client.ts +202 -0
- package/src/communityNotes.ts +194 -0
- package/src/credential.ts +100 -0
- package/src/defaults.ts +87 -0
- package/src/digest.ts +91 -0
- package/src/envelope.ts +432 -0
- package/src/errors.ts +165 -0
- package/src/express/index.ts +43 -0
- package/src/express/middleware.ts +387 -0
- package/src/express/store.ts +85 -0
- package/src/express/verify.ts +166 -0
- package/src/index.ts +107 -0
- package/src/outbox/client.ts +75 -0
- package/src/outbox/decision.ts +209 -0
- package/src/outbox/delivery.ts +163 -0
- package/src/outbox/enforcement/executor.ts +288 -0
- package/src/outbox/enforcement/planner.ts +377 -0
- package/src/outbox/evidence.ts +231 -0
- package/src/outbox/inbound.ts +140 -0
- package/src/outbox/index.ts +160 -0
- package/src/outbox/intake.ts +175 -0
- package/src/outbox/integration.ts +205 -0
- package/src/outbox/outbox/dispatcher.ts +131 -0
- package/src/outbox/outbox/service.ts +444 -0
- package/src/outbox/postgres/index.ts +59 -0
- package/src/outbox/postgres/registries.ts +177 -0
- package/src/outbox/postgres/reportColumns.ts +281 -0
- package/src/outbox/postgres/store/enforcement.ts +186 -0
- package/src/outbox/postgres/store/events.ts +112 -0
- package/src/outbox/postgres/store/index.ts +72 -0
- package/src/outbox/postgres/store/outbox.ts +273 -0
- package/src/outbox/postgres/store/reports.ts +349 -0
- package/src/outbox/postgres/store/transaction.ts +64 -0
- package/src/outbox/postgres/tables.ts +339 -0
- package/src/outbox/reconciliation.ts +199 -0
- package/src/outbox/reportStatus.ts +24 -0
- package/src/outbox/retention.ts +34 -0
- package/src/outbox/store/types.ts +528 -0
- package/src/outbox/types.ts +753 -0
- package/src/outbox/webhook.ts +165 -0
- package/src/reports.ts +138 -0
- package/src/testing/fixtures.ts +183 -0
- package/src/testing/index.ts +57 -0
- package/src/testing/sandbox.ts +469 -0
- package/src/testing/webhook-simulator.ts +166 -0
- package/src/transport.ts +250 -0
- package/src/webhookEndpoints.ts +256 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhookEndpoints.js","sourceRoot":"","sources":["../src/webhookEndpoints.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crowdsource.you/core",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "The CrowdSource integration: the API client at the root, and the Express webhook receiver, the transactional outbox and the test sandbox behind subpaths",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/esm/index.js",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./express": {
|
|
16
|
+
"types": "./dist/express/index.d.ts",
|
|
17
|
+
"import": "./dist/esm/express/index.js",
|
|
18
|
+
"require": "./dist/express/index.js",
|
|
19
|
+
"default": "./dist/express/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./outbox": {
|
|
22
|
+
"types": "./dist/outbox/index.d.ts",
|
|
23
|
+
"import": "./dist/esm/outbox/index.js",
|
|
24
|
+
"require": "./dist/outbox/index.js",
|
|
25
|
+
"default": "./dist/outbox/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./outbox/postgres": {
|
|
28
|
+
"types": "./dist/outbox/postgres/index.d.ts",
|
|
29
|
+
"import": "./dist/esm/outbox/postgres/index.js",
|
|
30
|
+
"require": "./dist/outbox/postgres/index.js",
|
|
31
|
+
"default": "./dist/outbox/postgres/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./testing": {
|
|
34
|
+
"types": "./dist/testing/index.d.ts",
|
|
35
|
+
"import": "./dist/esm/testing/index.js",
|
|
36
|
+
"require": "./dist/testing/index.js",
|
|
37
|
+
"default": "./dist/testing/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"typesVersions": {
|
|
42
|
+
"*": {
|
|
43
|
+
"express": ["./dist/express/index.d.ts"],
|
|
44
|
+
"outbox": ["./dist/outbox/index.d.ts"],
|
|
45
|
+
"outbox/postgres": ["./dist/outbox/postgres/index.d.ts"],
|
|
46
|
+
"testing": ["./dist/testing/index.d.ts"]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc && tsc -p tsconfig.esm.json && bun ../../scripts/write-esm-marker.mjs",
|
|
51
|
+
"dev": "tsc --watch",
|
|
52
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
53
|
+
"db:generate": "drizzle-kit generate",
|
|
54
|
+
"lint": "tsc --noEmit --noUnusedLocals --noUnusedParameters && tsc -p tsconfig.test.json",
|
|
55
|
+
"test": "bun run test:unit && bun run test:express && bun run test:testing && bun run test:outbox && bun run test:invariants && bun run test:table-type",
|
|
56
|
+
"test:unit": "node ../../node_modules/vitest/vitest.mjs run",
|
|
57
|
+
"test:express": "node ../../node_modules/vitest/vitest.mjs run -c vitest.express.config.ts",
|
|
58
|
+
"test:outbox": "node ../../node_modules/vitest/vitest.mjs run -c vitest.outbox.config.ts",
|
|
59
|
+
"test:testing": "node ../../node_modules/vitest/vitest.mjs run -c vitest.testing.config.ts",
|
|
60
|
+
"test:invariants": "bun ./scripts/test-invariants.mjs",
|
|
61
|
+
"test:table-type": "bun ./scripts/test-report-table-type.mjs",
|
|
62
|
+
"test:watch": "node ../../node_modules/vitest/vitest.mjs",
|
|
63
|
+
"prepublishOnly": "bun run build"
|
|
64
|
+
},
|
|
65
|
+
"keywords": [
|
|
66
|
+
"crowdsource",
|
|
67
|
+
"moderation",
|
|
68
|
+
"sdk",
|
|
69
|
+
"outbox",
|
|
70
|
+
"webhooks",
|
|
71
|
+
"express",
|
|
72
|
+
"postgresql"
|
|
73
|
+
],
|
|
74
|
+
"author": "OxyHQ",
|
|
75
|
+
"license": "MIT",
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "https://github.com/OxyHQ/CrowdSource",
|
|
79
|
+
"directory": "packages/core"
|
|
80
|
+
},
|
|
81
|
+
"homepage": "https://github.com/OxyHQ/CrowdSource/tree/main/packages/core#readme",
|
|
82
|
+
"bugs": "https://github.com/OxyHQ/CrowdSource/issues",
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "public"
|
|
85
|
+
},
|
|
86
|
+
"dependencies": {
|
|
87
|
+
"zod": "^4.6.5"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"@crowdsource.you/contracts": "1.1.0",
|
|
91
|
+
"@oxy.so/db": "^1.0.0",
|
|
92
|
+
"@types/express": "^5.0.6",
|
|
93
|
+
"@types/node": "^25.9.5",
|
|
94
|
+
"drizzle-kit": "^0.31.10",
|
|
95
|
+
"drizzle-orm": "^0.45.2",
|
|
96
|
+
"express": "^5.2.1",
|
|
97
|
+
"postgres": "^3.4.9",
|
|
98
|
+
"typescript": "^5.9.3",
|
|
99
|
+
"vitest": "^4.1.10"
|
|
100
|
+
},
|
|
101
|
+
"files": [
|
|
102
|
+
"dist/**/*",
|
|
103
|
+
"src/**/*",
|
|
104
|
+
"!src/**/__tests__/**"
|
|
105
|
+
],
|
|
106
|
+
"peerDependencies": {
|
|
107
|
+
"@crowdsource.you/contracts": "^1.1.0",
|
|
108
|
+
"@oxy.so/db": "^1.0.0",
|
|
109
|
+
"drizzle-orm": "^0.45.2",
|
|
110
|
+
"express": ">=4.18.0 <6",
|
|
111
|
+
"postgres": "^3.4.9"
|
|
112
|
+
},
|
|
113
|
+
"peerDependenciesMeta": {
|
|
114
|
+
"@oxy.so/db": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"drizzle-orm": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"express": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"postgres": {
|
|
124
|
+
"optional": true
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/cases.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /v1/cases/{id}` and `GET /v1/decisions/{id}` (§10.2).
|
|
3
|
+
*
|
|
4
|
+
* Both are look-ups by an id CrowdSource gave the application. There is no case
|
|
5
|
+
* search and there will not be one: "nobody chooses the case they review" is an
|
|
6
|
+
* invariant, and a list endpoint on the application API would be the first step
|
|
7
|
+
* towards a queue somebody browses.
|
|
8
|
+
*
|
|
9
|
+
* `decisions.get` is written against the route §10.2 defines. **The backend does
|
|
10
|
+
* not serve it yet** — nothing publishes decisions, because sortition, review
|
|
11
|
+
* and consensus are not built — so it answers 404 today. It is here because a
|
|
12
|
+
* client of a documented endpoint is not a stub, and because the webhook path
|
|
13
|
+
* (`case.decided`) is how an application learns about a decision anyway; this is
|
|
14
|
+
* the read-back for one it already has an id for.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { DecisionSchema, type Decision, type TaxonomyCode } from '@crowdsource.you/contracts';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
import { CrowdSourceTransportError } from './errors.js';
|
|
21
|
+
import type { Transport } from './transport.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The projection §10.2 returns for a case.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately not the case document. Priority score, review pool and reporter
|
|
27
|
+
* fingerprints never leave the service — an application that could read its own
|
|
28
|
+
* cases' priority could learn which signals move it.
|
|
29
|
+
*/
|
|
30
|
+
export interface CaseView {
|
|
31
|
+
readonly caseId: string;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
readonly subject: { readonly externalId: string; readonly type: string };
|
|
34
|
+
readonly policy: { readonly policySetId: string; readonly version: string };
|
|
35
|
+
readonly taxonomyVersion: string;
|
|
36
|
+
readonly allegationCodes: readonly (TaxonomyCode | (string & {}))[];
|
|
37
|
+
/** How many reports merged into this case. §7.3: many reports, one case. */
|
|
38
|
+
readonly reportCount: number;
|
|
39
|
+
readonly sensitivityClass: string;
|
|
40
|
+
readonly currentRevision: number;
|
|
41
|
+
readonly createdAt: string;
|
|
42
|
+
readonly updatedAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CaseViewSchema = z.looseObject({
|
|
46
|
+
caseId: z.string(),
|
|
47
|
+
status: z.string(),
|
|
48
|
+
subject: z.looseObject({ externalId: z.string(), type: z.string() }),
|
|
49
|
+
policy: z.looseObject({ policySetId: z.string(), version: z.string() }),
|
|
50
|
+
taxonomyVersion: z.string(),
|
|
51
|
+
allegationCodes: z.array(z.string()),
|
|
52
|
+
reportCount: z.number(),
|
|
53
|
+
sensitivityClass: z.string(),
|
|
54
|
+
currentRevision: z.number(),
|
|
55
|
+
createdAt: z.string(),
|
|
56
|
+
updatedAt: z.string(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export interface ReadOptions {
|
|
60
|
+
readonly signal?: AbortSignal;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class Cases {
|
|
64
|
+
private readonly transport: Transport;
|
|
65
|
+
|
|
66
|
+
constructor(transport: Transport) {
|
|
67
|
+
this.transport = transport;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async get(caseId: string, options: ReadOptions = {}): Promise<CaseView> {
|
|
71
|
+
const response = await this.transport.request<unknown>({
|
|
72
|
+
method: 'GET',
|
|
73
|
+
path: `/v1/cases/${encodeURIComponent(caseId)}`,
|
|
74
|
+
signal: options.signal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const parsed = CaseViewSchema.safeParse(response);
|
|
78
|
+
if (!parsed.success) {
|
|
79
|
+
throw new CrowdSourceTransportError(
|
|
80
|
+
'CrowdSource answered with a case this client does not recognise.',
|
|
81
|
+
{ retryable: false, cause: parsed.error },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return parsed.data;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class Decisions {
|
|
89
|
+
private readonly transport: Transport;
|
|
90
|
+
|
|
91
|
+
constructor(transport: Transport) {
|
|
92
|
+
this.transport = transport;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reads one immutable revision of a decision.
|
|
97
|
+
*
|
|
98
|
+
* A published revision is never edited (Appendix F); a later revision
|
|
99
|
+
* supersedes it and carries `supersedesDecisionId`. So a decision read twice
|
|
100
|
+
* is byte-identical, and an application that cached one never needs to
|
|
101
|
+
* invalidate it — it needs to notice a `decision.corrected` webhook.
|
|
102
|
+
*/
|
|
103
|
+
async get(decisionId: string, options: ReadOptions = {}): Promise<Decision> {
|
|
104
|
+
const response = await this.transport.request<unknown>({
|
|
105
|
+
method: 'GET',
|
|
106
|
+
path: `/v1/decisions/${encodeURIComponent(decisionId)}`,
|
|
107
|
+
signal: options.signal,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const parsed = DecisionSchema.safeParse(response);
|
|
111
|
+
if (!parsed.success) {
|
|
112
|
+
throw new CrowdSourceTransportError(
|
|
113
|
+
'CrowdSource answered with a decision this client does not recognise.',
|
|
114
|
+
{ retryable: false, cause: parsed.error },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return parsed.data;
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client.
|
|
3
|
+
*
|
|
4
|
+
* The whole design target is the length of the smallest integration:
|
|
5
|
+
*
|
|
6
|
+
* const crowdsource = new CrowdSource();
|
|
7
|
+
* await crowdsource.reports.create({ ... });
|
|
8
|
+
*
|
|
9
|
+
* Everything else has a default that is correct for the overwhelming majority of
|
|
10
|
+
* integrators and a way to override it for the rest. The service key comes from
|
|
11
|
+
* the environment, the base URL is the one deployment CrowdSource has, and the
|
|
12
|
+
* envelope, the policy version, the digests, the principal refs and the
|
|
13
|
+
* idempotency key are all composed rather than configured.
|
|
14
|
+
*
|
|
15
|
+
* This client is SERVER-SIDE ONLY. A service credential is the tenant's identity
|
|
16
|
+
* for its whole moderation stream; shipping one to a browser or a mobile bundle
|
|
17
|
+
* hands every user of the application the ability to file reports as the
|
|
18
|
+
* application, read its cases and exhaust its quota. The package depends on
|
|
19
|
+
* `node:crypto` and does not build for a browser, which is the intended
|
|
20
|
+
* outcome rather than a limitation to work around.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Cases, Decisions } from './cases.js';
|
|
24
|
+
import { CommunityNotes } from './communityNotes.js';
|
|
25
|
+
import { parseServiceKey, type ServiceCredential } from './credential.js';
|
|
26
|
+
import { DEFAULT_BASE_URL } from './defaults.js';
|
|
27
|
+
import { CrowdSourceConfigurationError } from './errors.js';
|
|
28
|
+
import { Reports } from './reports.js';
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
31
|
+
DEFAULT_TIMEOUT_MS,
|
|
32
|
+
Transport,
|
|
33
|
+
type FetchLike,
|
|
34
|
+
} from './transport.js';
|
|
35
|
+
import { WebhookEndpoints } from './webhookEndpoints.js';
|
|
36
|
+
|
|
37
|
+
/** The environment variable a zero-configuration integration reads. */
|
|
38
|
+
export const SERVICE_KEY_ENV_VAR = 'CROWDSOURCE_SERVICE_KEY';
|
|
39
|
+
|
|
40
|
+
/** Overrides the service host. Set only when pointing at a local backend. */
|
|
41
|
+
export const BASE_URL_ENV_VAR = 'CROWDSOURCE_BASE_URL';
|
|
42
|
+
|
|
43
|
+
export interface CrowdSourceOptions {
|
|
44
|
+
/**
|
|
45
|
+
* The service key CrowdSource issued, as one opaque string. Defaults to
|
|
46
|
+
* `process.env.CROWDSOURCE_SERVICE_KEY`.
|
|
47
|
+
*
|
|
48
|
+
* There is no `applicationId` option here or anywhere else. The application a
|
|
49
|
+
* report belongs to is read off this credential — see `credential.ts`.
|
|
50
|
+
*/
|
|
51
|
+
readonly serviceKey?: string;
|
|
52
|
+
/**
|
|
53
|
+
* A first-party Oxy service authenticating with NO CrowdSource credential.
|
|
54
|
+
*
|
|
55
|
+
* Return a current Oxy service token; it is asked for once per request
|
|
56
|
+
* attempt, so returning a cached token and refreshing it when it expires is
|
|
57
|
+
* the expected shape (`oxyServices.getServiceToken()` does exactly that).
|
|
58
|
+
*
|
|
59
|
+
* With this set, `serviceKey` is neither needed nor read. CrowdSource resolves
|
|
60
|
+
* the tenant from the Oxy application the token names, so nothing here has to
|
|
61
|
+
* be issued, stored or rotated by a person. Third parties keep the service
|
|
62
|
+
* key: they run where Oxy cannot vouch for them.
|
|
63
|
+
*/
|
|
64
|
+
readonly oxyToken?: () => string | Promise<string>;
|
|
65
|
+
readonly baseUrl?: string;
|
|
66
|
+
/** Per-attempt deadline. Default 10s. */
|
|
67
|
+
readonly timeoutMs?: number;
|
|
68
|
+
/** Attempts per call including the first, for retryable failures. Default 3. */
|
|
69
|
+
readonly maxAttempts?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Marks reports as coming from the application's own pre-production (§5.1
|
|
72
|
+
* `source.environment`). CrowdSource has one deployment; this is a property of
|
|
73
|
+
* the report, not a different host to talk to.
|
|
74
|
+
*
|
|
75
|
+
* Every report from a sandbox client must carry `submittedAt`, because the
|
|
76
|
+
* environment travels inside `source` and `source` cannot be composed without
|
|
77
|
+
* one — see `ReportInput.submittedAt` for why inventing that timestamp would
|
|
78
|
+
* turn every retry into a 409.
|
|
79
|
+
*/
|
|
80
|
+
readonly sandbox?: boolean;
|
|
81
|
+
/** Injected for tests and for the in-process sandbox. Defaults to global `fetch`. */
|
|
82
|
+
readonly fetch?: FetchLike;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class CrowdSource {
|
|
86
|
+
/**
|
|
87
|
+
* The application this client acts as.
|
|
88
|
+
*
|
|
89
|
+
* Read off the credential when there is one. With an Oxy token there is
|
|
90
|
+
* nothing to read it off — the mapping lives in CrowdSource — so it resolves
|
|
91
|
+
* on first use from `GET /v1/applications/me` and is remembered.
|
|
92
|
+
*/
|
|
93
|
+
readonly applicationId: string | Promise<string>;
|
|
94
|
+
|
|
95
|
+
readonly reports: Reports;
|
|
96
|
+
readonly cases: Cases;
|
|
97
|
+
readonly decisions: Decisions;
|
|
98
|
+
/** Where decisions get delivered, and the secret that signs them (§10.2). */
|
|
99
|
+
readonly webhookEndpoints: WebhookEndpoints;
|
|
100
|
+
/** Community notes: write, withdraw, draw to rate, rate, and the reads. */
|
|
101
|
+
readonly communityNotes: CommunityNotes;
|
|
102
|
+
|
|
103
|
+
constructor(options: CrowdSourceOptions = {}) {
|
|
104
|
+
/**
|
|
105
|
+
* Two ways to be an application, and exactly one of them is configured.
|
|
106
|
+
*
|
|
107
|
+
* The Oxy path is checked first so that a deployment which has BOTH — during
|
|
108
|
+
* the migration off shared secrets — uses the identity it can prove rather
|
|
109
|
+
* than the secret it still happens to hold. Removing the key is then the
|
|
110
|
+
* cleanup, not the cutover.
|
|
111
|
+
*/
|
|
112
|
+
const credential: ServiceCredential | null = options.oxyToken
|
|
113
|
+
? null
|
|
114
|
+
: parseServiceKey(options.serviceKey ?? process.env[SERVICE_KEY_ENV_VAR] ?? '');
|
|
115
|
+
|
|
116
|
+
const baseUrl = normalisedBaseUrl(
|
|
117
|
+
options.baseUrl ?? process.env[BASE_URL_ENV_VAR] ?? DEFAULT_BASE_URL,
|
|
118
|
+
);
|
|
119
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
120
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
121
|
+
if (typeof fetchImpl !== 'function') {
|
|
122
|
+
throw new CrowdSourceConfigurationError(
|
|
123
|
+
'This runtime has no global fetch. Pass one as the `fetch` option.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const oxyToken = options.oxyToken;
|
|
128
|
+
const transport = new Transport({
|
|
129
|
+
baseUrl,
|
|
130
|
+
bearerToken: credential ? () => credential.bearerToken : () => oxyToken!(),
|
|
131
|
+
timeoutMs,
|
|
132
|
+
maxAttempts: options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
133
|
+
fetch: fetchImpl,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.applicationId = credential ? credential.applicationId : lazyApplicationId(transport);
|
|
137
|
+
this.reports = new Reports({
|
|
138
|
+
transport,
|
|
139
|
+
applicationId: this.applicationId,
|
|
140
|
+
environment: options.sandbox === true ? 'sandbox' : 'production',
|
|
141
|
+
});
|
|
142
|
+
this.cases = new Cases(transport);
|
|
143
|
+
this.decisions = new Decisions(transport);
|
|
144
|
+
this.webhookEndpoints = new WebhookEndpoints(transport);
|
|
145
|
+
this.communityNotes = new CommunityNotes(transport);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Asks CrowdSource which application this client is, once.
|
|
152
|
+
*
|
|
153
|
+
* Only the Oxy-token path needs this: a service key carries the id inside it.
|
|
154
|
+
* The promise is created at construction and awaited wherever the id is used,
|
|
155
|
+
* so the lookup happens at most once per client and never blocks a caller that
|
|
156
|
+
* does not need it (community notes never do; a report does, because its
|
|
157
|
+
* envelope names the application and the server refuses one that disagrees).
|
|
158
|
+
*
|
|
159
|
+
* A failure is not swallowed into a placeholder id. An envelope carrying the
|
|
160
|
+
* wrong application is refused by the server anyway, and a client that invented
|
|
161
|
+
* one would turn a clear "we could not identify you" into a confusing 403 on
|
|
162
|
+
* every report.
|
|
163
|
+
*/
|
|
164
|
+
function lazyApplicationId(transport: Transport): Promise<string> {
|
|
165
|
+
let pending: Promise<string> | null = null;
|
|
166
|
+
const resolve = () => (pending ??= askApplicationId(transport));
|
|
167
|
+
// A thenable rather than a promise: nothing is requested until somebody
|
|
168
|
+
// awaits it, so a client that only reads community notes — which never name
|
|
169
|
+
// an application — makes no identity call at all. Awaiting it twice still
|
|
170
|
+
// makes one.
|
|
171
|
+
return { then: (onFulfilled, onRejected) => resolve().then(onFulfilled, onRejected) } as Promise<string>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function askApplicationId(transport: Transport): Promise<string> {
|
|
175
|
+
return transport
|
|
176
|
+
.request<{ applicationId?: unknown }>({ method: 'GET', path: '/v1/applications/me' })
|
|
177
|
+
.then((body) => {
|
|
178
|
+
const applicationId = body?.applicationId;
|
|
179
|
+
if (typeof applicationId !== 'string' || applicationId.length === 0) {
|
|
180
|
+
throw new CrowdSourceConfigurationError(
|
|
181
|
+
'CrowdSource did not name the application this token belongs to.',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return applicationId;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalisedBaseUrl(value: string): string {
|
|
189
|
+
let parsed: URL;
|
|
190
|
+
try {
|
|
191
|
+
parsed = new URL(value);
|
|
192
|
+
} catch {
|
|
193
|
+
throw new CrowdSourceConfigurationError(`'${value}' is not a usable CrowdSource base URL.`);
|
|
194
|
+
}
|
|
195
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {
|
|
196
|
+
throw new CrowdSourceConfigurationError(
|
|
197
|
+
'A CrowdSource base URL must be https. A service credential sent in clear is a credential you have to rotate.',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
// Trailing slashes are stripped so `${baseUrl}/v1/reports` never doubles up.
|
|
201
|
+
return parsed.origin + parsed.pathname.replace(/\/+$/, '');
|
|
202
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CommunityNoteAssignmentBatchSchema,
|
|
3
|
+
CommunityNoteListSchema,
|
|
4
|
+
CommunityNoteRatedListSchema,
|
|
5
|
+
CommunityNoteRatingSchema,
|
|
6
|
+
CommunityNoteSchema,
|
|
7
|
+
ShownCommunityNotesSchema,
|
|
8
|
+
type CommunityNote,
|
|
9
|
+
type CommunityNoteAssignment,
|
|
10
|
+
type CommunityNoteAssignmentRequest,
|
|
11
|
+
type CommunityNoteRatedList,
|
|
12
|
+
type CommunityNoteRating,
|
|
13
|
+
type CommunityNoteRatingSubmission,
|
|
14
|
+
type CommunityNoteSubmission,
|
|
15
|
+
} from '@crowdsource.you/contracts';
|
|
16
|
+
import type { z } from 'zod';
|
|
17
|
+
|
|
18
|
+
import { sha256Digest } from './digest.js';
|
|
19
|
+
import { CrowdSourceTransportError } from './errors.js';
|
|
20
|
+
import type { Transport } from './transport.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Community notes (`docs/architecture/community-notes.md`): reader-written context
|
|
24
|
+
* under the application's subjects, shown once raters of different viewpoints
|
|
25
|
+
* agree it is helpful.
|
|
26
|
+
*
|
|
27
|
+
* The application acts for its users, naming each by its own opaque principal id.
|
|
28
|
+
* Every write is retry-safe with no key from the caller where one can be derived
|
|
29
|
+
* from what the write IS — one note per writer per subject, one rating per rater
|
|
30
|
+
* per note — and the derived keys hash the principal ids, so an application's
|
|
31
|
+
* user ids never travel in a header a proxy might log. Drawing notes to rate has
|
|
32
|
+
* no such identity (two draws for one rater are two different requests), so that
|
|
33
|
+
* one method takes the key from the caller.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface CommunityNoteRequestOptions {
|
|
37
|
+
readonly idempotencyKey?: string;
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CommunityNoteReadOptions {
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A stable, header-safe digest of the parts that identify a write. */
|
|
46
|
+
function digestOf(...parts: readonly string[]): string {
|
|
47
|
+
return sha256Digest(JSON.stringify(parts)).replace('sha256:', '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class CommunityNotes {
|
|
51
|
+
private readonly transport: Transport;
|
|
52
|
+
|
|
53
|
+
constructor(transport: Transport) {
|
|
54
|
+
this.transport = transport;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Writes a note. A retry of the same writer's note on the same subject returns it. */
|
|
58
|
+
async write(submission: CommunityNoteSubmission, options: CommunityNoteRequestOptions = {}): Promise<CommunityNote> {
|
|
59
|
+
return this.parse(
|
|
60
|
+
CommunityNoteSchema,
|
|
61
|
+
await this.transport.request<unknown>({
|
|
62
|
+
method: 'POST',
|
|
63
|
+
path: '/v1/community-notes',
|
|
64
|
+
body: submission,
|
|
65
|
+
idempotencyKey:
|
|
66
|
+
options.idempotencyKey ??
|
|
67
|
+
`community-note.${digestOf(submission.externalSubjectId, submission.authorPrincipalId)}`,
|
|
68
|
+
signal: options.signal,
|
|
69
|
+
}),
|
|
70
|
+
'a community note',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Withdraws a note. Only its writer may; anyone else's note answers 404. */
|
|
75
|
+
async withdraw(
|
|
76
|
+
noteId: string,
|
|
77
|
+
authorPrincipalId: string,
|
|
78
|
+
options: CommunityNoteRequestOptions = {},
|
|
79
|
+
): Promise<CommunityNote> {
|
|
80
|
+
return this.parse(
|
|
81
|
+
CommunityNoteSchema,
|
|
82
|
+
await this.transport.request<unknown>({
|
|
83
|
+
method: 'POST',
|
|
84
|
+
path: `/v1/community-notes/${encodeURIComponent(noteId)}/withdraw`,
|
|
85
|
+
body: { authorPrincipalId },
|
|
86
|
+
idempotencyKey: options.idempotencyKey ?? `community-note-withdrawal.${digestOf(noteId)}`,
|
|
87
|
+
signal: options.signal,
|
|
88
|
+
}),
|
|
89
|
+
'a community note',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Draws notes for one rater to rate — the ONLY way to be allowed to rate one.
|
|
95
|
+
*
|
|
96
|
+
* `idempotencyKey` is required: a retry with the same key returns the same
|
|
97
|
+
* batch, and a new key is a new draw.
|
|
98
|
+
*/
|
|
99
|
+
async drawToRate(
|
|
100
|
+
request: CommunityNoteAssignmentRequest,
|
|
101
|
+
options: CommunityNoteRequestOptions & { readonly idempotencyKey: string },
|
|
102
|
+
): Promise<CommunityNoteAssignment[]> {
|
|
103
|
+
const batch = this.parse(
|
|
104
|
+
CommunityNoteAssignmentBatchSchema,
|
|
105
|
+
await this.transport.request<unknown>({
|
|
106
|
+
method: 'POST',
|
|
107
|
+
path: '/v1/community-notes/assignments',
|
|
108
|
+
body: request,
|
|
109
|
+
idempotencyKey: options.idempotencyKey,
|
|
110
|
+
signal: options.signal,
|
|
111
|
+
}),
|
|
112
|
+
'a batch of community note assignments',
|
|
113
|
+
);
|
|
114
|
+
return batch.assignments;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Rates a note assigned to the rater. Final: a second rating is refused. */
|
|
118
|
+
async rate(
|
|
119
|
+
noteId: string,
|
|
120
|
+
submission: CommunityNoteRatingSubmission,
|
|
121
|
+
options: CommunityNoteRequestOptions = {},
|
|
122
|
+
): Promise<CommunityNoteRating> {
|
|
123
|
+
return this.parse(
|
|
124
|
+
CommunityNoteRatingSchema,
|
|
125
|
+
await this.transport.request<unknown>({
|
|
126
|
+
method: 'POST',
|
|
127
|
+
path: `/v1/community-notes/${encodeURIComponent(noteId)}/ratings`,
|
|
128
|
+
body: submission,
|
|
129
|
+
idempotencyKey:
|
|
130
|
+
options.idempotencyKey ?? `community-note-rating.${digestOf(noteId, submission.raterPrincipalId)}`,
|
|
131
|
+
signal: options.signal,
|
|
132
|
+
}),
|
|
133
|
+
'a community note rating',
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The shown note, if any, for each of up to 50 subjects. */
|
|
138
|
+
async shown(externalSubjectIds: readonly string[], options: CommunityNoteReadOptions = {}): Promise<CommunityNote[]> {
|
|
139
|
+
const subjects = [...new Set(externalSubjectIds)];
|
|
140
|
+
if (subjects.length === 0) return [];
|
|
141
|
+
const response = this.parse(
|
|
142
|
+
ShownCommunityNotesSchema,
|
|
143
|
+
await this.transport.request<unknown>({
|
|
144
|
+
method: 'GET',
|
|
145
|
+
path: `/v1/community-notes/shown?subjects=${subjects.map(encodeURIComponent).join(',')}`,
|
|
146
|
+
signal: options.signal,
|
|
147
|
+
}),
|
|
148
|
+
'a shown community notes lookup',
|
|
149
|
+
);
|
|
150
|
+
return response.notes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A writer's own notes, newest first. */
|
|
154
|
+
async writtenBy(authorPrincipalId: string, options: CommunityNoteReadOptions = {}): Promise<CommunityNote[]> {
|
|
155
|
+
const response = this.parse(
|
|
156
|
+
CommunityNoteListSchema,
|
|
157
|
+
await this.transport.request<unknown>({
|
|
158
|
+
method: 'GET',
|
|
159
|
+
path: `/v1/community-notes/principals/${encodeURIComponent(authorPrincipalId)}/notes`,
|
|
160
|
+
signal: options.signal,
|
|
161
|
+
}),
|
|
162
|
+
'a list of community notes',
|
|
163
|
+
);
|
|
164
|
+
return response.notes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A rater's own ratings, each with the note it rated, newest first. */
|
|
168
|
+
async ratedBy(
|
|
169
|
+
raterPrincipalId: string,
|
|
170
|
+
options: CommunityNoteReadOptions = {},
|
|
171
|
+
): Promise<CommunityNoteRatedList['ratings']> {
|
|
172
|
+
const response = this.parse(
|
|
173
|
+
CommunityNoteRatedListSchema,
|
|
174
|
+
await this.transport.request<unknown>({
|
|
175
|
+
method: 'GET',
|
|
176
|
+
path: `/v1/community-notes/principals/${encodeURIComponent(raterPrincipalId)}/ratings`,
|
|
177
|
+
signal: options.signal,
|
|
178
|
+
}),
|
|
179
|
+
'a list of community note ratings',
|
|
180
|
+
);
|
|
181
|
+
return response.ratings;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private parse<S extends z.ZodType>(schema: S, response: unknown, what: string): z.infer<S> {
|
|
185
|
+
const parsed = schema.safeParse(response);
|
|
186
|
+
if (!parsed.success) {
|
|
187
|
+
throw new CrowdSourceTransportError(`CrowdSource answered with ${what} this client does not recognise.`, {
|
|
188
|
+
retryable: false,
|
|
189
|
+
cause: parsed.error,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return parsed.data;
|
|
193
|
+
}
|
|
194
|
+
}
|