@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,121 @@
|
|
|
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
|
+
import { Cases, Decisions } from './cases.js';
|
|
23
|
+
import { CommunityNotes } from './communityNotes.js';
|
|
24
|
+
import { parseServiceKey } from './credential.js';
|
|
25
|
+
import { DEFAULT_BASE_URL } from './defaults.js';
|
|
26
|
+
import { CrowdSourceConfigurationError } from './errors.js';
|
|
27
|
+
import { Reports } from './reports.js';
|
|
28
|
+
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS, Transport, } from './transport.js';
|
|
29
|
+
import { WebhookEndpoints } from './webhookEndpoints.js';
|
|
30
|
+
/** The environment variable a zero-configuration integration reads. */
|
|
31
|
+
export const SERVICE_KEY_ENV_VAR = 'CROWDSOURCE_SERVICE_KEY';
|
|
32
|
+
/** Overrides the service host. Set only when pointing at a local backend. */
|
|
33
|
+
export const BASE_URL_ENV_VAR = 'CROWDSOURCE_BASE_URL';
|
|
34
|
+
export class CrowdSource {
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
/**
|
|
37
|
+
* Two ways to be an application, and exactly one of them is configured.
|
|
38
|
+
*
|
|
39
|
+
* The Oxy path is checked first so that a deployment which has BOTH — during
|
|
40
|
+
* the migration off shared secrets — uses the identity it can prove rather
|
|
41
|
+
* than the secret it still happens to hold. Removing the key is then the
|
|
42
|
+
* cleanup, not the cutover.
|
|
43
|
+
*/
|
|
44
|
+
const credential = options.oxyToken
|
|
45
|
+
? null
|
|
46
|
+
: parseServiceKey(options.serviceKey ?? process.env[SERVICE_KEY_ENV_VAR] ?? '');
|
|
47
|
+
const baseUrl = normalisedBaseUrl(options.baseUrl ?? process.env[BASE_URL_ENV_VAR] ?? DEFAULT_BASE_URL);
|
|
48
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
49
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
50
|
+
if (typeof fetchImpl !== 'function') {
|
|
51
|
+
throw new CrowdSourceConfigurationError('This runtime has no global fetch. Pass one as the `fetch` option.');
|
|
52
|
+
}
|
|
53
|
+
const oxyToken = options.oxyToken;
|
|
54
|
+
const transport = new Transport({
|
|
55
|
+
baseUrl,
|
|
56
|
+
bearerToken: credential ? () => credential.bearerToken : () => oxyToken(),
|
|
57
|
+
timeoutMs,
|
|
58
|
+
maxAttempts: options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
59
|
+
fetch: fetchImpl,
|
|
60
|
+
});
|
|
61
|
+
this.applicationId = credential ? credential.applicationId : lazyApplicationId(transport);
|
|
62
|
+
this.reports = new Reports({
|
|
63
|
+
transport,
|
|
64
|
+
applicationId: this.applicationId,
|
|
65
|
+
environment: options.sandbox === true ? 'sandbox' : 'production',
|
|
66
|
+
});
|
|
67
|
+
this.cases = new Cases(transport);
|
|
68
|
+
this.decisions = new Decisions(transport);
|
|
69
|
+
this.webhookEndpoints = new WebhookEndpoints(transport);
|
|
70
|
+
this.communityNotes = new CommunityNotes(transport);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Asks CrowdSource which application this client is, once.
|
|
75
|
+
*
|
|
76
|
+
* Only the Oxy-token path needs this: a service key carries the id inside it.
|
|
77
|
+
* The promise is created at construction and awaited wherever the id is used,
|
|
78
|
+
* so the lookup happens at most once per client and never blocks a caller that
|
|
79
|
+
* does not need it (community notes never do; a report does, because its
|
|
80
|
+
* envelope names the application and the server refuses one that disagrees).
|
|
81
|
+
*
|
|
82
|
+
* A failure is not swallowed into a placeholder id. An envelope carrying the
|
|
83
|
+
* wrong application is refused by the server anyway, and a client that invented
|
|
84
|
+
* one would turn a clear "we could not identify you" into a confusing 403 on
|
|
85
|
+
* every report.
|
|
86
|
+
*/
|
|
87
|
+
function lazyApplicationId(transport) {
|
|
88
|
+
let pending = null;
|
|
89
|
+
const resolve = () => (pending ?? (pending = askApplicationId(transport)));
|
|
90
|
+
// A thenable rather than a promise: nothing is requested until somebody
|
|
91
|
+
// awaits it, so a client that only reads community notes — which never name
|
|
92
|
+
// an application — makes no identity call at all. Awaiting it twice still
|
|
93
|
+
// makes one.
|
|
94
|
+
return { then: (onFulfilled, onRejected) => resolve().then(onFulfilled, onRejected) };
|
|
95
|
+
}
|
|
96
|
+
function askApplicationId(transport) {
|
|
97
|
+
return transport
|
|
98
|
+
.request({ method: 'GET', path: '/v1/applications/me' })
|
|
99
|
+
.then((body) => {
|
|
100
|
+
const applicationId = body?.applicationId;
|
|
101
|
+
if (typeof applicationId !== 'string' || applicationId.length === 0) {
|
|
102
|
+
throw new CrowdSourceConfigurationError('CrowdSource did not name the application this token belongs to.');
|
|
103
|
+
}
|
|
104
|
+
return applicationId;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function normalisedBaseUrl(value) {
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = new URL(value);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new CrowdSourceConfigurationError(`'${value}' is not a usable CrowdSource base URL.`);
|
|
114
|
+
}
|
|
115
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {
|
|
116
|
+
throw new CrowdSourceConfigurationError('A CrowdSource base URL must be https. A service credential sent in clear is a credential you have to rotate.');
|
|
117
|
+
}
|
|
118
|
+
// Trailing slashes are stripped so `${baseUrl}/v1/reports` never doubles up.
|
|
119
|
+
return parsed.origin + parsed.pathname.replace(/\/+$/, '');
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAA0B,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,SAAS,GAEV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,uEAAuE;AACvE,MAAM,CAAC,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AA4CvD,MAAM,OAAO,WAAW;IAkBtB,YAAY,UAA8B,EAAE;QAC1C;;;;;;;WAOG;QACH,MAAM,UAAU,GAA6B,OAAO,CAAC,QAAQ;YAC3D,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC;QAElF,MAAM,OAAO,GAAG,iBAAiB,CAC/B,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,gBAAgB,CACrE,CAAC;QACF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAAC;QAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;QACpD,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;YACpC,MAAM,IAAI,6BAA6B,CACrC,mEAAmE,CACpE,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QAClC,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;YAC9B,OAAO;YACP,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,QAAS,EAAE;YAC1E,SAAS;YACT,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,oBAAoB;YACxD,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QAC1F,IAAI,CAAC,OAAO,GAAG,IAAI,OAAO,CAAC;YACzB,SAAS;YACT,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,WAAW,EAAE,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;SACjE,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,gBAAgB,GAAG,IAAI,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACxD,IAAI,CAAC,cAAc,GAAG,IAAI,cAAc,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;CACF;AAGD;;;;;;;;;;;;;GAaG;AACH,SAAS,iBAAiB,CAAC,SAAoB;IAC7C,IAAI,OAAO,GAA2B,IAAI,CAAC;IAC3C,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC,OAAO,KAAP,OAAO,GAAK,gBAAgB,CAAC,SAAS,CAAC,EAAC,CAAC;IAChE,wEAAwE;IACxE,4EAA4E;IAC5E,0EAA0E;IAC1E,aAAa;IACb,OAAO,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,EAAqB,CAAC;AAC3G,CAAC;AAED,SAAS,gBAAgB,CAAC,SAAoB;IAC5C,OAAO,SAAS;SACb,OAAO,CAA8B,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,qBAAqB,EAAE,CAAC;SACpF,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACb,MAAM,aAAa,GAAG,IAAI,EAAE,aAAa,CAAC;QAC1C,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,6BAA6B,CACrC,iEAAiE,CAClE,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC;IACvB,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,6BAA6B,CAAC,IAAI,KAAK,yCAAyC,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpE,MAAM,IAAI,6BAA6B,CACrC,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IACD,6EAA6E;IAC7E,OAAO,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CommunityNoteAssignmentBatchSchema, CommunityNoteListSchema, CommunityNoteRatedListSchema, CommunityNoteRatingSchema, CommunityNoteSchema, ShownCommunityNotesSchema, } from '@crowdsource.you/contracts';
|
|
2
|
+
import { sha256Digest } from './digest.js';
|
|
3
|
+
import { CrowdSourceTransportError } from './errors.js';
|
|
4
|
+
/** A stable, header-safe digest of the parts that identify a write. */
|
|
5
|
+
function digestOf(...parts) {
|
|
6
|
+
return sha256Digest(JSON.stringify(parts)).replace('sha256:', '');
|
|
7
|
+
}
|
|
8
|
+
export class CommunityNotes {
|
|
9
|
+
constructor(transport) {
|
|
10
|
+
this.transport = transport;
|
|
11
|
+
}
|
|
12
|
+
/** Writes a note. A retry of the same writer's note on the same subject returns it. */
|
|
13
|
+
async write(submission, options = {}) {
|
|
14
|
+
return this.parse(CommunityNoteSchema, await this.transport.request({
|
|
15
|
+
method: 'POST',
|
|
16
|
+
path: '/v1/community-notes',
|
|
17
|
+
body: submission,
|
|
18
|
+
idempotencyKey: options.idempotencyKey ??
|
|
19
|
+
`community-note.${digestOf(submission.externalSubjectId, submission.authorPrincipalId)}`,
|
|
20
|
+
signal: options.signal,
|
|
21
|
+
}), 'a community note');
|
|
22
|
+
}
|
|
23
|
+
/** Withdraws a note. Only its writer may; anyone else's note answers 404. */
|
|
24
|
+
async withdraw(noteId, authorPrincipalId, options = {}) {
|
|
25
|
+
return this.parse(CommunityNoteSchema, await this.transport.request({
|
|
26
|
+
method: 'POST',
|
|
27
|
+
path: `/v1/community-notes/${encodeURIComponent(noteId)}/withdraw`,
|
|
28
|
+
body: { authorPrincipalId },
|
|
29
|
+
idempotencyKey: options.idempotencyKey ?? `community-note-withdrawal.${digestOf(noteId)}`,
|
|
30
|
+
signal: options.signal,
|
|
31
|
+
}), 'a community note');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Draws notes for one rater to rate — the ONLY way to be allowed to rate one.
|
|
35
|
+
*
|
|
36
|
+
* `idempotencyKey` is required: a retry with the same key returns the same
|
|
37
|
+
* batch, and a new key is a new draw.
|
|
38
|
+
*/
|
|
39
|
+
async drawToRate(request, options) {
|
|
40
|
+
const batch = this.parse(CommunityNoteAssignmentBatchSchema, await this.transport.request({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: '/v1/community-notes/assignments',
|
|
43
|
+
body: request,
|
|
44
|
+
idempotencyKey: options.idempotencyKey,
|
|
45
|
+
signal: options.signal,
|
|
46
|
+
}), 'a batch of community note assignments');
|
|
47
|
+
return batch.assignments;
|
|
48
|
+
}
|
|
49
|
+
/** Rates a note assigned to the rater. Final: a second rating is refused. */
|
|
50
|
+
async rate(noteId, submission, options = {}) {
|
|
51
|
+
return this.parse(CommunityNoteRatingSchema, await this.transport.request({
|
|
52
|
+
method: 'POST',
|
|
53
|
+
path: `/v1/community-notes/${encodeURIComponent(noteId)}/ratings`,
|
|
54
|
+
body: submission,
|
|
55
|
+
idempotencyKey: options.idempotencyKey ?? `community-note-rating.${digestOf(noteId, submission.raterPrincipalId)}`,
|
|
56
|
+
signal: options.signal,
|
|
57
|
+
}), 'a community note rating');
|
|
58
|
+
}
|
|
59
|
+
/** The shown note, if any, for each of up to 50 subjects. */
|
|
60
|
+
async shown(externalSubjectIds, options = {}) {
|
|
61
|
+
const subjects = [...new Set(externalSubjectIds)];
|
|
62
|
+
if (subjects.length === 0)
|
|
63
|
+
return [];
|
|
64
|
+
const response = this.parse(ShownCommunityNotesSchema, await this.transport.request({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
path: `/v1/community-notes/shown?subjects=${subjects.map(encodeURIComponent).join(',')}`,
|
|
67
|
+
signal: options.signal,
|
|
68
|
+
}), 'a shown community notes lookup');
|
|
69
|
+
return response.notes;
|
|
70
|
+
}
|
|
71
|
+
/** A writer's own notes, newest first. */
|
|
72
|
+
async writtenBy(authorPrincipalId, options = {}) {
|
|
73
|
+
const response = this.parse(CommunityNoteListSchema, await this.transport.request({
|
|
74
|
+
method: 'GET',
|
|
75
|
+
path: `/v1/community-notes/principals/${encodeURIComponent(authorPrincipalId)}/notes`,
|
|
76
|
+
signal: options.signal,
|
|
77
|
+
}), 'a list of community notes');
|
|
78
|
+
return response.notes;
|
|
79
|
+
}
|
|
80
|
+
/** A rater's own ratings, each with the note it rated, newest first. */
|
|
81
|
+
async ratedBy(raterPrincipalId, options = {}) {
|
|
82
|
+
const response = this.parse(CommunityNoteRatedListSchema, await this.transport.request({
|
|
83
|
+
method: 'GET',
|
|
84
|
+
path: `/v1/community-notes/principals/${encodeURIComponent(raterPrincipalId)}/ratings`,
|
|
85
|
+
signal: options.signal,
|
|
86
|
+
}), 'a list of community note ratings');
|
|
87
|
+
return response.ratings;
|
|
88
|
+
}
|
|
89
|
+
parse(schema, response, what) {
|
|
90
|
+
const parsed = schema.safeParse(response);
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
throw new CrowdSourceTransportError(`CrowdSource answered with ${what} this client does not recognise.`, {
|
|
93
|
+
retryable: false,
|
|
94
|
+
cause: parsed.error,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return parsed.data;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=communityNotes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"communityNotes.js","sourceRoot":"","sources":["../../src/communityNotes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The service key, and the only place `applicationId` may come from.
|
|
3
|
+
*
|
|
4
|
+
* Appendix F: "`applicationId` comes from the credential, never from the request
|
|
5
|
+
* body." The backend already honours that — it derives the tenant from the
|
|
6
|
+
* presented token and 403s an envelope naming a different application. But the
|
|
7
|
+
* Case Envelope contract still REQUIRES `applicationId` on the wire, so a client
|
|
8
|
+
* has to put one there, and the question this module answers is where it gets it
|
|
9
|
+
* from without ever asking the integrator.
|
|
10
|
+
*
|
|
11
|
+
* The answer is that it comes out of the credential, literally. CrowdSource
|
|
12
|
+
* issues three values together (`provisioning.service.ts`
|
|
13
|
+
* `IssuedCredential`): the application the credential belongs to, the credential
|
|
14
|
+
* id, and the secret. Joined into ONE opaque string, they are a single
|
|
15
|
+
* environment variable an integrator pastes and never reads — and the
|
|
16
|
+
* `applicationId` in an envelope is then a value the client READ OFF THE
|
|
17
|
+
* CREDENTIAL, not a parameter anyone can pass. There is no `applicationId`
|
|
18
|
+
* option on the client, none on any method, and none an envelope input can
|
|
19
|
+
* carry; supplying one is not "discouraged", it is unexpressible.
|
|
20
|
+
*
|
|
21
|
+
* `:` is the separator because the contract's identifier grammar excludes it and
|
|
22
|
+
* credential secrets are base64url, so the split is unambiguous for every value
|
|
23
|
+
* CrowdSource can issue. A `.` separator would not be: `IdentifierSchema` allows
|
|
24
|
+
* dots inside an id.
|
|
25
|
+
*
|
|
26
|
+
* NOTE FOR THE CONSOLE: `issueApplicationCredential` returns the three values
|
|
27
|
+
* separately and the bearer token as `<credentialId>.<secret>`. Whatever surface
|
|
28
|
+
* shows an integrator their key must show `formatServiceKey(issued)` — the
|
|
29
|
+
* composite below — or the integrator has two values to configure instead of
|
|
30
|
+
* one, and the client has no way to know its own application.
|
|
31
|
+
*/
|
|
32
|
+
import { IdentifierSchema } from '@crowdsource.you/contracts';
|
|
33
|
+
import { CrowdSourceConfigurationError } from './errors.js';
|
|
34
|
+
const SERVICE_KEY_SEPARATOR = ':';
|
|
35
|
+
/** The three values `issueApplicationCredential` returns, as one opaque key. */
|
|
36
|
+
export function formatServiceKey(issued) {
|
|
37
|
+
return [issued.applicationId, issued.credentialId, issued.secret].join(SERVICE_KEY_SEPARATOR);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses a service key.
|
|
41
|
+
*
|
|
42
|
+
* Every rejection names which part is wrong and none of them echoes the secret:
|
|
43
|
+
* a configuration error is read from a log, and a log line is the second most
|
|
44
|
+
* common way a credential leaks after a screenshot.
|
|
45
|
+
*/
|
|
46
|
+
export function parseServiceKey(serviceKey) {
|
|
47
|
+
const trimmed = serviceKey.trim();
|
|
48
|
+
if (!trimmed) {
|
|
49
|
+
throw new CrowdSourceConfigurationError('The CrowdSource service key is empty.');
|
|
50
|
+
}
|
|
51
|
+
const parts = trimmed.split(SERVICE_KEY_SEPARATOR);
|
|
52
|
+
if (parts.length !== 3) {
|
|
53
|
+
throw new CrowdSourceConfigurationError(`A CrowdSource service key is three colon-separated parts (applicationId:credentialId:secret); this one has ${parts.length}.`);
|
|
54
|
+
}
|
|
55
|
+
const [applicationId, credentialId, secret] = parts;
|
|
56
|
+
if (!IdentifierSchema.safeParse(applicationId).success) {
|
|
57
|
+
throw new CrowdSourceConfigurationError('The first part of a CrowdSource service key must be the applicationId it was issued for.');
|
|
58
|
+
}
|
|
59
|
+
if (!IdentifierSchema.safeParse(credentialId).success) {
|
|
60
|
+
throw new CrowdSourceConfigurationError('The second part of a CrowdSource service key must be the credentialId it was issued for.');
|
|
61
|
+
}
|
|
62
|
+
if (!secret) {
|
|
63
|
+
throw new CrowdSourceConfigurationError('The third part of a CrowdSource service key must be the credential secret.');
|
|
64
|
+
}
|
|
65
|
+
return { applicationId, credentialId, bearerToken: `${credentialId}.${secret}` };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=credential.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential.js","sourceRoot":"","sources":["../../src/credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9D,OAAO,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAE5D,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAelC,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,MAIhC;IACC,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;AAChG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,6BAA6B,CAAC,uCAAuC,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,6BAA6B,CACrC,8GAA8G,KAAK,CAAC,MAAM,GAAG,CAC9H,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,aAAa,EAAE,YAAY,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IAEpD,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC;QACvD,MAAM,IAAI,6BAA6B,CACrC,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC;QACtD,MAAM,IAAI,6BAA6B,CACrC,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,6BAA6B,CACrC,4EAA4E,CAC7E,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,YAAY,IAAI,MAAM,EAAE,EAAE,CAAC;AACnF,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an integrator does not have to declare, and why each default is safe to
|
|
3
|
+
* apply without being asked.
|
|
4
|
+
*
|
|
5
|
+
* The product requirement is near-zero configuration: an application should be
|
|
6
|
+
* able to report a piece of content with a credential and the object itself. The
|
|
7
|
+
* plan's requirement pulling the other way is §6.4 — every decision records the
|
|
8
|
+
* policy version it was decided under, and a policy update must never silently
|
|
9
|
+
* change what a past decision meant.
|
|
10
|
+
*
|
|
11
|
+
* Both hold only if the default is a PINNED VERSION rather than "whatever is
|
|
12
|
+
* current". `DEFAULT_POLICY` below names a specific immutable published version.
|
|
13
|
+
* When CrowdSource publishes a newer baseline, this constant changes in a
|
|
14
|
+
* release of this package and integrators adopt it by upgrading — which is
|
|
15
|
+
* visible in a lockfile, reviewable in a diff, and dated. The alternative
|
|
16
|
+
* (resolving "latest" server-side) would move the policy version under an
|
|
17
|
+
* application that never changed a line, and would silently split
|
|
18
|
+
* `caseDedupKey` — §7.3 makes the policy version part of that key, so the day it
|
|
19
|
+
* moved, two reports about one post would open two cases.
|
|
20
|
+
*/
|
|
21
|
+
/** Where the service lives. One deployment; there is no sandbox host. */
|
|
22
|
+
export const DEFAULT_BASE_URL = 'https://api.crowdsource.oxy.so';
|
|
23
|
+
/**
|
|
24
|
+
* The policy set a report is evaluated under when the application declares
|
|
25
|
+
* none: the baseline CrowdSource ships over its own universal taxonomy (§6.3).
|
|
26
|
+
*
|
|
27
|
+
* Must stay equal to `BASELINE_POLICY_SET_ID` / `BASELINE_POLICY_VERSION` in the
|
|
28
|
+
* backend's `modules/policy/policyBaseline.ts`, which is the registry that
|
|
29
|
+
* resolves it. A drift is not a type error and not a test failure anywhere else:
|
|
30
|
+
* it is a 422 on every zero-config report, in production, at the moment of
|
|
31
|
+
* upgrade. `__tests__/defaults.test.ts` reads that file and asserts the pair.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_POLICY = Object.freeze({
|
|
34
|
+
policySetId: 'crowdsource.baseline',
|
|
35
|
+
version: '2026.07',
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* §13.6's default retention: 30 days after a final decision, configurable by
|
|
39
|
+
* policy.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_RETENTION_DAYS = 30;
|
|
42
|
+
/**
|
|
43
|
+
* Allegation codes that must never reach a community jury (§7.5).
|
|
44
|
+
*
|
|
45
|
+
* §7.5 routes child sexual abuse and potentially illegal material to a
|
|
46
|
+
* specialist team under legal protocol, and non-consensual intimate material to
|
|
47
|
+
* specialist review. An integrator who never read §7.5 still gets that: an
|
|
48
|
+
* envelope alleging one of these is composed with `allowCommunityReview: false`,
|
|
49
|
+
* and an application that explicitly asks for `true` alongside one of them is
|
|
50
|
+
* REFUSED rather than quietly corrected — the belief that this material can be
|
|
51
|
+
* community-reviewed is the defect, and silently fixing the field would leave
|
|
52
|
+
* the belief in place.
|
|
53
|
+
*
|
|
54
|
+
* This is a floor, never a ceiling. Triage may route a case away from the
|
|
55
|
+
* community for reasons this list cannot see; nothing here can route one
|
|
56
|
+
* towards it.
|
|
57
|
+
*/
|
|
58
|
+
export const COMMUNITY_REVIEW_FORBIDDEN_ALLEGATIONS = Object.freeze([
|
|
59
|
+
'child_safety.sexualization',
|
|
60
|
+
'child_safety.grooming',
|
|
61
|
+
'child_safety.exploitation',
|
|
62
|
+
'sexual_content.non_consensual',
|
|
63
|
+
'sexual_content.exploitation',
|
|
64
|
+
]);
|
|
65
|
+
const FORBIDDEN_SET = new Set(COMMUNITY_REVIEW_FORBIDDEN_ALLEGATIONS);
|
|
66
|
+
/** The allegations in `codes` that §7.5 keeps away from a community jury. */
|
|
67
|
+
export function allegationsForbiddingCommunityReview(codes) {
|
|
68
|
+
return codes.filter((code) => FORBIDDEN_SET.has(code));
|
|
69
|
+
}
|
|
70
|
+
/** The privacy terms of a report that declares none. */
|
|
71
|
+
export function defaultPrivacy(allegationCodes) {
|
|
72
|
+
return {
|
|
73
|
+
retentionDays: DEFAULT_RETENTION_DAYS,
|
|
74
|
+
allowCommunityReview: allegationsForbiddingCommunityReview(allegationCodes).length === 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,gCAAgC,CAAC;AAEjE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkB,MAAM,CAAC,MAAM,CAAC;IACzD,WAAW,EAAE,sBAAsB;IACnC,OAAO,EAAE,SAAS;CACnB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,sCAAsC,GAA4B,MAAM,CAAC,MAAM,CAAC;IAC3F,4BAA4B;IAC5B,uBAAuB;IACvB,2BAA2B;IAC3B,+BAA+B;IAC/B,6BAA6B;CAC9B,CAAC,CAAC;AAEH,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC,sCAAsC,CAAC,CAAC;AAE3F,6EAA6E;AAC7E,MAAM,UAAU,oCAAoC,CAClD,KAA8B;IAE9B,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,cAAc,CAAC,eAAwC;IACrE,OAAO;QACL,aAAa,EAAE,sBAAsB;QACrC,oBAAoB,EAAE,oCAAoC,CAAC,eAAe,CAAC,CAAC,MAAM,KAAK,CAAC;KACzF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource digests (§5.2 `sha256`, §5.6 "SHA 256 of each resource").
|
|
3
|
+
*
|
|
4
|
+
* Every inline resource in an envelope must carry a digest of "the exact
|
|
5
|
+
* representation reviewed", and asking an integrator to compute one is asking
|
|
6
|
+
* them to get canonical serialisation right — which they will do differently
|
|
7
|
+
* from the next integrator, and differently again from themselves next quarter.
|
|
8
|
+
* So the client computes it, from one rule applied to every resource type:
|
|
9
|
+
*
|
|
10
|
+
* sha256:<hex of SHA-256 over canonicalJson({ type, data }) in UTF-8>
|
|
11
|
+
*
|
|
12
|
+
* `type` is inside the digest so a `text` resource and a `metadata` resource
|
|
13
|
+
* carrying identical fields do not produce the same digest. `custom` hashes its
|
|
14
|
+
* registered `schemaId` and `payload` for the same reason.
|
|
15
|
+
*
|
|
16
|
+
* Determinism is not a nicety here, it is the deduplication invariant. §7.3
|
|
17
|
+
* requires two people reporting the same version of the same post to land on ONE
|
|
18
|
+
* case, and the server's `caseDedupKey` is computed over a projection of the
|
|
19
|
+
* envelope that INCLUDES every resource verbatim, digest and all. A digest that
|
|
20
|
+
* varied with key order — or with which reporter sent it — would give the same
|
|
21
|
+
* post two cases, and "one penalty per incident" would be broken by a JSON
|
|
22
|
+
* serialiser.
|
|
23
|
+
*
|
|
24
|
+
* Asset-backed resources are the exception the contract already makes: their
|
|
25
|
+
* bytes live outside the envelope and `asset.sha256` is the digest of those
|
|
26
|
+
* bytes, so nothing here computes one for them.
|
|
27
|
+
*/
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
/**
|
|
30
|
+
* JSON with object keys in a fixed order and `undefined` members dropped.
|
|
31
|
+
*
|
|
32
|
+
* Array order is PRESERVED. In a conversation's `messageResourceIds` the order
|
|
33
|
+
* is the meaning, and the backend's own canonicaliser makes the same choice for
|
|
34
|
+
* the same reason.
|
|
35
|
+
*/
|
|
36
|
+
export function canonicalJson(value) {
|
|
37
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
38
|
+
return JSON.stringify(value);
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === 'number') {
|
|
41
|
+
if (!Number.isFinite(value)) {
|
|
42
|
+
throw new TypeError('A resource cannot carry a non-finite number: it has no JSON form.');
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
if (isCanonicalArray(value)) {
|
|
47
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
|
|
48
|
+
}
|
|
49
|
+
const members = Object.keys(value)
|
|
50
|
+
.sort()
|
|
51
|
+
.flatMap((key) => {
|
|
52
|
+
const member = value[key];
|
|
53
|
+
return member === undefined ? [] : [`${JSON.stringify(key)}:${canonicalJson(member)}`];
|
|
54
|
+
});
|
|
55
|
+
return `{${members.join(',')}}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A written-out predicate rather than a bare `Array.isArray`, because
|
|
59
|
+
* `Array.isArray` narrows `readonly T[]` in its TRUE branch only — the false
|
|
60
|
+
* branch keeps the array in the union and the object branch below stops
|
|
61
|
+
* type-checking.
|
|
62
|
+
*/
|
|
63
|
+
function isCanonicalArray(value) {
|
|
64
|
+
return Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
/** `sha256:<64 lowercase hex>` — the only digest form the contract accepts. */
|
|
67
|
+
export function sha256Digest(bytes) {
|
|
68
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
69
|
+
}
|
|
70
|
+
/** The digest of an inline resource's reviewable representation. */
|
|
71
|
+
export function resourceDigest(reviewable) {
|
|
72
|
+
return sha256Digest(Buffer.from(canonicalJson(reviewable), 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAWzC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,KAAqB;IACjD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9E,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,SAAS,CAAC,mEAAmE,CAAC,CAAC;QAC3F,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACrE,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SAC/B,IAAI,EAAE;SACN,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACf,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1B,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEL,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,KAAqB;IAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,KAA0B;IACrD,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACtE,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,cAAc,CAAC,UAA0B;IACvD,OAAO,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtE,CAAC"}
|