@heavybit/shared-lib 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/access/index.js +118 -0
- package/dist/cjs/access/index.js.map +1 -0
- package/dist/cjs/alerting/index.js +56 -0
- package/dist/cjs/alerting/index.js.map +1 -0
- package/dist/cjs/cache/index.js +167 -0
- package/dist/cjs/cache/index.js.map +1 -0
- package/dist/cjs/common/async.js +40 -0
- package/dist/cjs/common/async.js.map +1 -0
- package/dist/cjs/common/audit.js +21 -0
- package/dist/cjs/common/audit.js.map +1 -0
- package/dist/cjs/common/crypto.js +75 -0
- package/dist/cjs/common/crypto.js.map +1 -0
- package/dist/cjs/common/errors.js +90 -0
- package/dist/cjs/common/errors.js.map +1 -0
- package/dist/cjs/common/id.js +40 -0
- package/dist/cjs/common/id.js.map +1 -0
- package/dist/cjs/common/index.js +28 -0
- package/dist/cjs/common/index.js.map +1 -0
- package/dist/cjs/common/jwt.js +59 -0
- package/dist/cjs/common/jwt.js.map +1 -0
- package/dist/cjs/common/password.js +38 -0
- package/dist/cjs/common/password.js.map +1 -0
- package/dist/cjs/common/phone.js +42 -0
- package/dist/cjs/common/phone.js.map +1 -0
- package/dist/cjs/common/response.js +32 -0
- package/dist/cjs/common/response.js.map +1 -0
- package/dist/cjs/common/text.js +46 -0
- package/dist/cjs/common/text.js.map +1 -0
- package/dist/cjs/common/time.js +109 -0
- package/dist/cjs/common/time.js.map +1 -0
- package/dist/cjs/config/index.js +96 -0
- package/dist/cjs/config/index.js.map +1 -0
- package/dist/cjs/contracts/editorial-draft.js +111 -0
- package/dist/cjs/contracts/editorial-draft.js.map +1 -0
- package/dist/cjs/contracts/index.js +20 -0
- package/dist/cjs/contracts/index.js.map +1 -0
- package/dist/cjs/contracts/notification-dispatch.js +92 -0
- package/dist/cjs/contracts/notification-dispatch.js.map +1 -0
- package/dist/cjs/contracts/upload-import.js +54 -0
- package/dist/cjs/contracts/upload-import.js.map +1 -0
- package/dist/cjs/database/index.js +57 -0
- package/dist/cjs/database/index.js.map +1 -0
- package/dist/cjs/express/auth.js +72 -0
- package/dist/cjs/express/auth.js.map +1 -0
- package/dist/cjs/express/health.js +31 -0
- package/dist/cjs/express/health.js.map +1 -0
- package/dist/cjs/express/index.js +20 -0
- package/dist/cjs/express/index.js.map +1 -0
- package/dist/cjs/express/middleware.js +95 -0
- package/dist/cjs/express/middleware.js.map +1 -0
- package/dist/cjs/http/index.js +90 -0
- package/dist/cjs/http/index.js.map +1 -0
- package/dist/cjs/http/safe-fetch.js +199 -0
- package/dist/cjs/http/safe-fetch.js.map +1 -0
- package/dist/cjs/identity/index.js +117 -0
- package/dist/cjs/identity/index.js.map +1 -0
- package/dist/cjs/index.js +29 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/knex/index.js +76 -0
- package/dist/cjs/knex/index.js.map +1 -0
- package/dist/cjs/logging/index.js +75 -0
- package/dist/cjs/logging/index.js.map +1 -0
- package/dist/cjs/messaging/connection.js +115 -0
- package/dist/cjs/messaging/connection.js.map +1 -0
- package/dist/cjs/messaging/consumer.js +134 -0
- package/dist/cjs/messaging/consumer.js.map +1 -0
- package/dist/cjs/messaging/envelope.js +23 -0
- package/dist/cjs/messaging/envelope.js.map +1 -0
- package/dist/cjs/messaging/idempotency.js +57 -0
- package/dist/cjs/messaging/idempotency.js.map +1 -0
- package/dist/cjs/messaging/index.js +62 -0
- package/dist/cjs/messaging/index.js.map +1 -0
- package/dist/cjs/messaging/monitor.js +48 -0
- package/dist/cjs/messaging/monitor.js.map +1 -0
- package/dist/cjs/messaging/outbox.js +135 -0
- package/dist/cjs/messaging/outbox.js.map +1 -0
- package/dist/cjs/messaging/publisher.js +149 -0
- package/dist/cjs/messaging/publisher.js.map +1 -0
- package/dist/cjs/messaging/rpc.js +125 -0
- package/dist/cjs/messaging/rpc.js.map +1 -0
- package/dist/cjs/messaging/staff-notifier.js +78 -0
- package/dist/cjs/messaging/staff-notifier.js.map +1 -0
- package/dist/cjs/messaging/topology.js +22 -0
- package/dist/cjs/messaging/topology.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/rate-limit/index.js +61 -0
- package/dist/cjs/rate-limit/index.js.map +1 -0
- package/dist/cjs/scheduling/index.js +88 -0
- package/dist/cjs/scheduling/index.js.map +1 -0
- package/dist/cjs/security/index.js +39 -0
- package/dist/cjs/security/index.js.map +1 -0
- package/dist/cjs/socket/index.js +53 -0
- package/dist/cjs/socket/index.js.map +1 -0
- package/dist/cjs/testing/index.js +73 -0
- package/dist/cjs/testing/index.js.map +1 -0
- package/dist/cjs/types/index.js +3 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/validation/index.js +41 -0
- package/dist/cjs/validation/index.js.map +1 -0
- package/dist/esm/access/index.js +104 -0
- package/dist/esm/access/index.js.map +1 -0
- package/dist/esm/alerting/index.js +52 -0
- package/dist/esm/alerting/index.js.map +1 -0
- package/dist/esm/cache/index.js +163 -0
- package/dist/esm/cache/index.js.map +1 -0
- package/dist/esm/common/async.js +33 -0
- package/dist/esm/common/async.js.map +1 -0
- package/dist/esm/common/audit.js +18 -0
- package/dist/esm/common/audit.js.map +1 -0
- package/dist/esm/common/crypto.js +64 -0
- package/dist/esm/common/crypto.js.map +1 -0
- package/dist/esm/common/errors.js +75 -0
- package/dist/esm/common/errors.js.map +1 -0
- package/dist/esm/common/id.js +33 -0
- package/dist/esm/common/id.js.map +1 -0
- package/dist/esm/common/index.js +12 -0
- package/dist/esm/common/index.js.map +1 -0
- package/dist/esm/common/jwt.js +49 -0
- package/dist/esm/common/jwt.js.map +1 -0
- package/dist/esm/common/password.js +30 -0
- package/dist/esm/common/password.js.map +1 -0
- package/dist/esm/common/phone.js +33 -0
- package/dist/esm/common/phone.js.map +1 -0
- package/dist/esm/common/response.js +24 -0
- package/dist/esm/common/response.js.map +1 -0
- package/dist/esm/common/text.js +39 -0
- package/dist/esm/common/text.js.map +1 -0
- package/dist/esm/common/time.js +96 -0
- package/dist/esm/common/time.js.map +1 -0
- package/dist/esm/config/index.js +88 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/contracts/editorial-draft.js +108 -0
- package/dist/esm/contracts/editorial-draft.js.map +1 -0
- package/dist/esm/contracts/index.js +4 -0
- package/dist/esm/contracts/index.js.map +1 -0
- package/dist/esm/contracts/notification-dispatch.js +89 -0
- package/dist/esm/contracts/notification-dispatch.js.map +1 -0
- package/dist/esm/contracts/upload-import.js +51 -0
- package/dist/esm/contracts/upload-import.js.map +1 -0
- package/dist/esm/database/index.js +48 -0
- package/dist/esm/database/index.js.map +1 -0
- package/dist/esm/express/auth.js +63 -0
- package/dist/esm/express/auth.js.map +1 -0
- package/dist/esm/express/health.js +27 -0
- package/dist/esm/express/health.js.map +1 -0
- package/dist/esm/express/index.js +4 -0
- package/dist/esm/express/index.js.map +1 -0
- package/dist/esm/express/middleware.js +84 -0
- package/dist/esm/express/middleware.js.map +1 -0
- package/dist/esm/http/index.js +73 -0
- package/dist/esm/http/index.js.map +1 -0
- package/dist/esm/http/safe-fetch.js +193 -0
- package/dist/esm/http/safe-fetch.js.map +1 -0
- package/dist/esm/identity/index.js +111 -0
- package/dist/esm/identity/index.js.map +1 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/knex/index.js +66 -0
- package/dist/esm/knex/index.js.map +1 -0
- package/dist/esm/logging/index.js +67 -0
- package/dist/esm/logging/index.js.map +1 -0
- package/dist/esm/messaging/connection.js +108 -0
- package/dist/esm/messaging/connection.js.map +1 -0
- package/dist/esm/messaging/consumer.js +130 -0
- package/dist/esm/messaging/consumer.js.map +1 -0
- package/dist/esm/messaging/envelope.js +19 -0
- package/dist/esm/messaging/envelope.js.map +1 -0
- package/dist/esm/messaging/idempotency.js +52 -0
- package/dist/esm/messaging/idempotency.js.map +1 -0
- package/dist/esm/messaging/index.js +45 -0
- package/dist/esm/messaging/index.js.map +1 -0
- package/dist/esm/messaging/monitor.js +44 -0
- package/dist/esm/messaging/monitor.js.map +1 -0
- package/dist/esm/messaging/outbox.js +129 -0
- package/dist/esm/messaging/outbox.js.map +1 -0
- package/dist/esm/messaging/publisher.js +142 -0
- package/dist/esm/messaging/publisher.js.map +1 -0
- package/dist/esm/messaging/rpc.js +120 -0
- package/dist/esm/messaging/rpc.js.map +1 -0
- package/dist/esm/messaging/staff-notifier.js +73 -0
- package/dist/esm/messaging/staff-notifier.js.map +1 -0
- package/dist/esm/messaging/topology.js +16 -0
- package/dist/esm/messaging/topology.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/rate-limit/index.js +56 -0
- package/dist/esm/rate-limit/index.js.map +1 -0
- package/dist/esm/scheduling/index.js +83 -0
- package/dist/esm/scheduling/index.js.map +1 -0
- package/dist/esm/security/index.js +36 -0
- package/dist/esm/security/index.js.map +1 -0
- package/dist/esm/socket/index.js +47 -0
- package/dist/esm/socket/index.js.map +1 -0
- package/dist/esm/testing/index.js +66 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/types/index.js +2 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/validation/index.js +36 -0
- package/dist/esm/validation/index.js.map +1 -0
- package/dist/types/access/index.d.ts +50 -0
- package/dist/types/access/index.d.ts.map +1 -0
- package/dist/types/alerting/index.d.ts +43 -0
- package/dist/types/alerting/index.d.ts.map +1 -0
- package/dist/types/cache/index.d.ts +45 -0
- package/dist/types/cache/index.d.ts.map +1 -0
- package/dist/types/common/async.d.ts +14 -0
- package/dist/types/common/async.d.ts.map +1 -0
- package/dist/types/common/audit.d.ts +17 -0
- package/dist/types/common/audit.d.ts.map +1 -0
- package/dist/types/common/crypto.d.ts +18 -0
- package/dist/types/common/crypto.d.ts.map +1 -0
- package/dist/types/common/errors.d.ts +71 -0
- package/dist/types/common/errors.d.ts.map +1 -0
- package/dist/types/common/id.d.ts +14 -0
- package/dist/types/common/id.d.ts.map +1 -0
- package/dist/types/common/index.d.ts +12 -0
- package/dist/types/common/index.d.ts.map +1 -0
- package/dist/types/common/jwt.d.ts +28 -0
- package/dist/types/common/jwt.d.ts.map +1 -0
- package/dist/types/common/password.d.ts +4 -0
- package/dist/types/common/password.d.ts.map +1 -0
- package/dist/types/common/phone.d.ts +10 -0
- package/dist/types/common/phone.d.ts.map +1 -0
- package/dist/types/common/response.d.ts +12 -0
- package/dist/types/common/response.d.ts.map +1 -0
- package/dist/types/common/text.d.ts +10 -0
- package/dist/types/common/text.d.ts.map +1 -0
- package/dist/types/common/time.d.ts +40 -0
- package/dist/types/common/time.d.ts.map +1 -0
- package/dist/types/config/index.d.ts +115 -0
- package/dist/types/config/index.d.ts.map +1 -0
- package/dist/types/contracts/editorial-draft.d.ts +577 -0
- package/dist/types/contracts/editorial-draft.d.ts.map +1 -0
- package/dist/types/contracts/index.d.ts +4 -0
- package/dist/types/contracts/index.d.ts.map +1 -0
- package/dist/types/contracts/notification-dispatch.d.ts +175 -0
- package/dist/types/contracts/notification-dispatch.d.ts.map +1 -0
- package/dist/types/contracts/upload-import.d.ts +96 -0
- package/dist/types/contracts/upload-import.d.ts.map +1 -0
- package/dist/types/database/index.d.ts +25 -0
- package/dist/types/database/index.d.ts.map +1 -0
- package/dist/types/express/auth.d.ts +27 -0
- package/dist/types/express/auth.d.ts.map +1 -0
- package/dist/types/express/health.d.ts +14 -0
- package/dist/types/express/health.d.ts.map +1 -0
- package/dist/types/express/index.d.ts +4 -0
- package/dist/types/express/index.d.ts.map +1 -0
- package/dist/types/express/middleware.d.ts +20 -0
- package/dist/types/express/middleware.d.ts.map +1 -0
- package/dist/types/http/index.d.ts +34 -0
- package/dist/types/http/index.d.ts.map +1 -0
- package/dist/types/http/safe-fetch.d.ts +58 -0
- package/dist/types/http/safe-fetch.d.ts.map +1 -0
- package/dist/types/identity/index.d.ts +51 -0
- package/dist/types/identity/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/knex/index.d.ts +25 -0
- package/dist/types/knex/index.d.ts.map +1 -0
- package/dist/types/logging/index.d.ts +22 -0
- package/dist/types/logging/index.d.ts.map +1 -0
- package/dist/types/messaging/connection.d.ts +48 -0
- package/dist/types/messaging/connection.d.ts.map +1 -0
- package/dist/types/messaging/consumer.d.ts +56 -0
- package/dist/types/messaging/consumer.d.ts.map +1 -0
- package/dist/types/messaging/envelope.d.ts +16 -0
- package/dist/types/messaging/envelope.d.ts.map +1 -0
- package/dist/types/messaging/idempotency.d.ts +29 -0
- package/dist/types/messaging/idempotency.d.ts.map +1 -0
- package/dist/types/messaging/index.d.ts +39 -0
- package/dist/types/messaging/index.d.ts.map +1 -0
- package/dist/types/messaging/monitor.d.ts +23 -0
- package/dist/types/messaging/monitor.d.ts.map +1 -0
- package/dist/types/messaging/outbox.d.ts +51 -0
- package/dist/types/messaging/outbox.d.ts.map +1 -0
- package/dist/types/messaging/publisher.d.ts +84 -0
- package/dist/types/messaging/publisher.d.ts.map +1 -0
- package/dist/types/messaging/rpc.d.ts +46 -0
- package/dist/types/messaging/rpc.d.ts.map +1 -0
- package/dist/types/messaging/staff-notifier.d.ts +56 -0
- package/dist/types/messaging/staff-notifier.d.ts.map +1 -0
- package/dist/types/messaging/topology.d.ts +23 -0
- package/dist/types/messaging/topology.d.ts.map +1 -0
- package/dist/types/rate-limit/index.d.ts +28 -0
- package/dist/types/rate-limit/index.d.ts.map +1 -0
- package/dist/types/scheduling/index.d.ts +33 -0
- package/dist/types/scheduling/index.d.ts.map +1 -0
- package/dist/types/security/index.d.ts +20 -0
- package/dist/types/security/index.d.ts.map +1 -0
- package/dist/types/socket/index.d.ts +30 -0
- package/dist/types/socket/index.d.ts.map +1 -0
- package/dist/types/testing/index.d.ts +41 -0
- package/dist/types/testing/index.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +65 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/validation/index.d.ts +34 -0
- package/dist/types/validation/index.d.ts.map +1 -0
- package/package.json +189 -4
- package/README.md +0 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { idSchema } from '../validation/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The newswire → editorial draft hand-off (design Appendix A.4 plus `paragraphFactIds` and
|
|
5
|
+
* `image.uploadId`, decision R14). newswire-service stages it through its outbox; editorial-service
|
|
6
|
+
* validates it with the same schema before creating the story. One contract for both sides.
|
|
7
|
+
*/
|
|
8
|
+
export const EDITORIAL_DRAFT_SUBMIT = 'editorial:draft.submit';
|
|
9
|
+
/** Field limits of the contract (editorial's story columns). */
|
|
10
|
+
export const EDITORIAL_DRAFT_LIMITS = {
|
|
11
|
+
headline: 200,
|
|
12
|
+
excerpt: 400,
|
|
13
|
+
tags: 6,
|
|
14
|
+
seoTitle: 70,
|
|
15
|
+
seoDescription: 170,
|
|
16
|
+
};
|
|
17
|
+
export const DRAFT_SOURCE_ROLES = ['primary', 'corroborating'];
|
|
18
|
+
export const DRAFT_RISKS = ['low', 'medium', 'high'];
|
|
19
|
+
export const GATE_VERDICTS = ['pass', 'review', 'block'];
|
|
20
|
+
/**
|
|
21
|
+
* Every quality gate newswire runs on a draft, in design order. A submitted draft carries one result per
|
|
22
|
+
* gate; editorial treats a missing one as not passed (auto-publish never runs on an unchecked draft).
|
|
23
|
+
*/
|
|
24
|
+
export const QUALITY_GATE_NAMES = [
|
|
25
|
+
'originality', 'grounding', 'quote_limits', 'corroboration', 'conflicts',
|
|
26
|
+
'sensitivity', 'category', 'freshness', 'duplicate_of_published', 'length_format',
|
|
27
|
+
];
|
|
28
|
+
export const FACT_KINDS = ['event', 'person', 'date', 'place', 'number', 'quote', 'context'];
|
|
29
|
+
const text = z.string().trim().min(1);
|
|
30
|
+
const httpsUrl = z
|
|
31
|
+
.string()
|
|
32
|
+
.url()
|
|
33
|
+
.refine((u) => u.startsWith('https://'), 'Source URL must use https');
|
|
34
|
+
const draftSourceSchema = z.object({
|
|
35
|
+
name: text.max(160),
|
|
36
|
+
url: httpsUrl,
|
|
37
|
+
publishedAt: z.string().datetime({ offset: true }).optional(),
|
|
38
|
+
role: z.enum(DRAFT_SOURCE_ROLES),
|
|
39
|
+
});
|
|
40
|
+
const gateResultSchema = z.object({
|
|
41
|
+
gate: text,
|
|
42
|
+
verdict: z.enum(GATE_VERDICTS),
|
|
43
|
+
reasons: z.array(z.string()),
|
|
44
|
+
metrics: z.record(z.string(), z.number()).optional(),
|
|
45
|
+
});
|
|
46
|
+
const factSupportSchema = z.object({ itemId: idSchema, paragraph: z.number().int().min(1) });
|
|
47
|
+
const factSchema = z.object({
|
|
48
|
+
id: text,
|
|
49
|
+
statement: text,
|
|
50
|
+
kind: z.enum(FACT_KINDS),
|
|
51
|
+
entities: z.array(z.string()),
|
|
52
|
+
supportedBy: z.array(factSupportSchema).min(1),
|
|
53
|
+
conflictsWith: z.array(z.string()),
|
|
54
|
+
independentSources: z.number().int().min(0),
|
|
55
|
+
});
|
|
56
|
+
const quoteSchema = factSupportSchema.extend({ text, speaker: text });
|
|
57
|
+
const conflictSchema = z.object({ description: text, factIds: z.array(z.string()) });
|
|
58
|
+
export const editorialDraftSubmitSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
draftId: idSchema,
|
|
61
|
+
clusterId: idSchema,
|
|
62
|
+
headline: text.max(EDITORIAL_DRAFT_LIMITS.headline),
|
|
63
|
+
excerpt: text.max(EDITORIAL_DRAFT_LIMITS.excerpt),
|
|
64
|
+
paragraphs: z.array(text).min(1),
|
|
65
|
+
/** The fact-sheet fact ids each paragraph asserts (same length as `paragraphs`). */
|
|
66
|
+
paragraphFactIds: z.array(z.array(z.string())),
|
|
67
|
+
category: text.max(60),
|
|
68
|
+
tags: z.array(text.max(40)).max(EDITORIAL_DRAFT_LIMITS.tags),
|
|
69
|
+
seo: z.object({
|
|
70
|
+
title: text.max(EDITORIAL_DRAFT_LIMITS.seoTitle),
|
|
71
|
+
description: text.max(EDITORIAL_DRAFT_LIMITS.seoDescription),
|
|
72
|
+
}),
|
|
73
|
+
social: z.object({ facebook: text, x: text, whatsapp: text, instagram: text }),
|
|
74
|
+
sources: z.array(draftSourceSchema).min(1),
|
|
75
|
+
/** An imported or profile cover (upload-service id), or null when the draft has none. */
|
|
76
|
+
image: z.object({ uploadId: idSchema, credit: text.max(160), alt: text.max(255) }).nullable(),
|
|
77
|
+
relatedProfileIds: z.array(idSchema),
|
|
78
|
+
suggestedProfileIds: z.array(idSchema),
|
|
79
|
+
review: z.object({
|
|
80
|
+
confidence: z.number().min(0).max(1),
|
|
81
|
+
risk: z.enum(DRAFT_RISKS),
|
|
82
|
+
riskReasons: z.array(z.string()),
|
|
83
|
+
gates: z.array(gateResultSchema),
|
|
84
|
+
factSheet: z.object({
|
|
85
|
+
facts: z.array(factSchema),
|
|
86
|
+
quotes: z.array(quoteSchema),
|
|
87
|
+
conflicts: z.array(conflictSchema),
|
|
88
|
+
}),
|
|
89
|
+
/** Prompt slug → version used to produce the draft. */
|
|
90
|
+
promptVersions: z.record(z.string(), z.number().int().positive()),
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
.superRefine((draft, ctx) => {
|
|
94
|
+
if (draft.paragraphFactIds.length !== draft.paragraphs.length) {
|
|
95
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['paragraphFactIds'], message: 'Must have one entry per paragraph' });
|
|
96
|
+
}
|
|
97
|
+
const factIds = new Set(draft.review.factSheet.facts.map((f) => f.id));
|
|
98
|
+
draft.paragraphFactIds.forEach((ids, i) => {
|
|
99
|
+
for (const id of ids) {
|
|
100
|
+
if (!factIds.has(id))
|
|
101
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['paragraphFactIds', i], message: `Unknown fact id ${id}` });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (draft.sources.filter((s) => s.role === 'primary').length !== 1) {
|
|
105
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['sources'], message: 'Exactly one source must be primary' });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=editorial-draft.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editorial-draft.js","sourceRoot":"","sources":["../../../src/contracts/editorial-draft.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/contracts/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* `notification:dispatch` — the one command every Showbiz service stages (through its outbox) to have
|
|
4
|
+
* notification-service render a seeded template and deliver it by email and/or the in-app feed. No
|
|
5
|
+
* other service sends email itself. One contract for both sides: producers type their payload with it
|
|
6
|
+
* and notification-service validates against it, so the two cannot drift.
|
|
7
|
+
*/
|
|
8
|
+
export const NOTIFICATION_DISPATCH = 'notification:dispatch';
|
|
9
|
+
/** Channels a dispatch may ask for. `in_app` needs a recipient `userId` (the bell feed is per user). */
|
|
10
|
+
export const DISPATCH_CHANNELS = ['email', 'in_app'];
|
|
11
|
+
/**
|
|
12
|
+
* What a dispatch is about. Account mail (`security`, `account`, `team`, `marketing`) goes to a user;
|
|
13
|
+
* `staff` is an operational alert to a configured staff address.
|
|
14
|
+
*/
|
|
15
|
+
export const NOTIFICATION_CATEGORIES = ['security', 'account', 'team', 'marketing', 'staff'];
|
|
16
|
+
export const DISPATCH_LIMITS = {
|
|
17
|
+
template: 100,
|
|
18
|
+
email: 254,
|
|
19
|
+
name: 200,
|
|
20
|
+
dataKeys: 40,
|
|
21
|
+
dataKey: 64,
|
|
22
|
+
dataValue: 2000,
|
|
23
|
+
secretKeys: 10,
|
|
24
|
+
permission: 100,
|
|
25
|
+
};
|
|
26
|
+
/** Template variables are flat scalars: they are printed into the copy, never serialised objects. */
|
|
27
|
+
export const dispatchValueSchema = z.union([z.string().max(DISPATCH_LIMITS.dataValue), z.number().finite(), z.boolean(), z.null()]);
|
|
28
|
+
export const dispatchRecipientSchema = z.object({
|
|
29
|
+
email: z.string().trim().email().max(DISPATCH_LIMITS.email),
|
|
30
|
+
name: z.string().trim().max(DISPATCH_LIMITS.name).optional(),
|
|
31
|
+
userId: z.string().max(36).nullish(),
|
|
32
|
+
locale: z.string().max(20).nullish(),
|
|
33
|
+
/** IANA zone; time variables are rendered in it. */
|
|
34
|
+
timezone: z.string().max(64).nullish(),
|
|
35
|
+
});
|
|
36
|
+
const dataKey = z.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/, 'Use a template variable name').max(DISPATCH_LIMITS.dataKey);
|
|
37
|
+
export const notificationDispatchSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
/** Seeded `notification_templates.slug`. */
|
|
40
|
+
template: z.string().trim().min(1).max(DISPATCH_LIMITS.template),
|
|
41
|
+
category: z.enum(NOTIFICATION_CATEGORIES),
|
|
42
|
+
recipient: dispatchRecipientSchema,
|
|
43
|
+
channels: z.array(z.enum(DISPATCH_CHANNELS)).min(1).max(DISPATCH_CHANNELS.length),
|
|
44
|
+
data: z
|
|
45
|
+
.record(dataKey, dispatchValueSchema)
|
|
46
|
+
.default({})
|
|
47
|
+
.refine((data) => Object.keys(data).length <= DISPATCH_LIMITS.dataKeys, `At most ${DISPATCH_LIMITS.dataKeys} variables`),
|
|
48
|
+
/**
|
|
49
|
+
* Keys of `data` holding a one-time secret (a sign-in code, a reset or confirm link). They are used to
|
|
50
|
+
* render the email and nothing else: notification-service never stores them and never puts them in the
|
|
51
|
+
* in-app feed, and email-service masks them in what it keeps.
|
|
52
|
+
*/
|
|
53
|
+
secretKeys: z.array(dataKey).max(DISPATCH_LIMITS.secretKeys).default([]),
|
|
54
|
+
/** Security and staff alerts are always delivered, whatever the recipient's preferences. */
|
|
55
|
+
bypassPreferences: z.boolean().optional(),
|
|
56
|
+
/** The permission staff recipients hold; informational until recipients are resolved by permission. */
|
|
57
|
+
targetPermission: z.string().max(DISPATCH_LIMITS.permission).optional(),
|
|
58
|
+
})
|
|
59
|
+
.superRefine((dispatch, ctx) => {
|
|
60
|
+
for (const key of dispatch.secretKeys) {
|
|
61
|
+
if (!(key in dispatch.data)) {
|
|
62
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['secretKeys'], message: `Secret key "${key}" is not in data` });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* The seeded templates Showbiz services dispatch, with the variables (`data` keys) each dispatch carries.
|
|
68
|
+
* Producers send exactly these keys; a seeded template may use any subset (notification-service dead-letters
|
|
69
|
+
* a dispatch missing a variable its template uses). Both sides test against this table.
|
|
70
|
+
*/
|
|
71
|
+
export const DISPATCH_TEMPLATE_VARIABLES = {
|
|
72
|
+
// auth-admin-service: account and security mail
|
|
73
|
+
'account-activation': ['name', 'code', 'expiresInHours'],
|
|
74
|
+
'login-otp': ['name', 'code', 'expiresInMinutes', 'device'],
|
|
75
|
+
'password-reset': ['name', 'resetUrl', 'expiresInMinutes'],
|
|
76
|
+
'staff-invitation': ['name', 'resetUrl', 'expiresInMinutes'],
|
|
77
|
+
'password-changed': ['name', 'changedAt'],
|
|
78
|
+
'new-device-login': ['name', 'device', 'ip', 'country', 'occurredAt', 'confirmUrl', 'disputeUrl'],
|
|
79
|
+
'team-member-added': ['name', 'profileName', 'role', 'invitedBy'],
|
|
80
|
+
'account-suspended': ['name', 'reason'],
|
|
81
|
+
'welcome-listener': ['name'],
|
|
82
|
+
'welcome-creator': ['name'],
|
|
83
|
+
// ai / newswire / editorial: staff alerts
|
|
84
|
+
'ai_budget_threshold': ['requester', 'period', 'threshold_pct', 'spent_usd', 'limit_usd'],
|
|
85
|
+
'newswire_source_unhealthy': ['source_name', 'source_slug', 'consecutive_failures', 'last_error', 'last_success_at'],
|
|
86
|
+
'newswire_block_rate_high': ['blocked', 'total', 'block_rate_pct', 'window_hours'],
|
|
87
|
+
'autopublish_audit_alert': ['problem_rate_pct', 'problem_count', 'sample_count', 'window_days', 'action_taken'],
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=notification-dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-dispatch.js","sourceRoot":"","sources":["../../../src/contracts/notification-dispatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { idSchema } from '../validation/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Licensed image import by URL (Newswire R5, decision R39). newswire-service (and later
|
|
5
|
+
* editorial-service) sends `upload:import`; upload-service fetches the file and answers with an
|
|
6
|
+
* `upload.import.completed` or `upload.import.failed` event. One contract for both sides.
|
|
7
|
+
*/
|
|
8
|
+
export const UPLOAD_IMPORT = 'upload:import';
|
|
9
|
+
export const UPLOAD_IMPORT_EVENTS = {
|
|
10
|
+
COMPLETED: 'upload.import.completed',
|
|
11
|
+
FAILED: 'upload.import.failed',
|
|
12
|
+
};
|
|
13
|
+
/** Upload policies an import may use. */
|
|
14
|
+
export const UPLOAD_IMPORT_POLICIES = ['news_image'];
|
|
15
|
+
/** Field limits of the contract (upload-service's `uploads` columns). */
|
|
16
|
+
export const UPLOAD_IMPORT_LIMITS = {
|
|
17
|
+
url: 2048,
|
|
18
|
+
ownerType: 50,
|
|
19
|
+
licenceHolder: 160,
|
|
20
|
+
licenceBasis: 255,
|
|
21
|
+
reason: 255,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* `upload:import` data. The URL is only length-checked here: upload-service rejects a non-https or
|
|
25
|
+
* unsafe URL itself, so the sender hears about it through a `failed` event instead of a dead letter.
|
|
26
|
+
*/
|
|
27
|
+
export const uploadImportCommandSchema = z.object({
|
|
28
|
+
/** ULID the outcome event echoes back; upload-service is idempotent on it. */
|
|
29
|
+
importId: idSchema,
|
|
30
|
+
url: z.string().min(1).max(UPLOAD_IMPORT_LIMITS.url),
|
|
31
|
+
policy: z.enum(UPLOAD_IMPORT_POLICIES),
|
|
32
|
+
/** Echoed in the outcome only; the imported upload is a system row with no owner. */
|
|
33
|
+
ownerType: z.string().min(1).max(UPLOAD_IMPORT_LIMITS.ownerType),
|
|
34
|
+
ownerId: idSchema,
|
|
35
|
+
licence: z.object({
|
|
36
|
+
holder: z.string().trim().min(1).max(UPLOAD_IMPORT_LIMITS.licenceHolder),
|
|
37
|
+
basis: z.string().trim().min(1).max(UPLOAD_IMPORT_LIMITS.licenceBasis),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
export const uploadImportCompletedSchema = z.object({
|
|
41
|
+
importId: idSchema,
|
|
42
|
+
uploadId: idSchema,
|
|
43
|
+
ownerType: z.string().min(1).max(UPLOAD_IMPORT_LIMITS.ownerType),
|
|
44
|
+
ownerId: idSchema,
|
|
45
|
+
});
|
|
46
|
+
/** `reason` is a machine code (`too_large`, `http_404`, …; see upload-service `ImportFailureReason`). */
|
|
47
|
+
export const uploadImportFailedSchema = z.object({
|
|
48
|
+
importId: idSchema,
|
|
49
|
+
reason: z.string().min(1).max(UPLOAD_IMPORT_LIMITS.reason),
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=upload-import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-import.js","sourceRoot":"","sources":["../../../src/contracts/upload-import.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS,EAAE,yBAAyB;IACpC,MAAM,EAAE,sBAAsB;CACtB,CAAC;AAEX,yCAAyC;AACzC,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,YAAY,CAAU,CAAC;AAG9D,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,GAAG,EAAE,IAAI;IACT,SAAS,EAAE,EAAE;IACb,aAAa,EAAE,GAAG;IAClB,YAAY,EAAE,GAAG;IACjB,MAAM,EAAE,GAAG;CACH,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,8EAA8E;IAC9E,QAAQ,EAAE,QAAQ;IAClB,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,GAAG,CAAC;IACpD,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC;IACtC,qFAAqF;IACrF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,SAAS,CAAC;IAChE,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,aAAa,CAAC;QACxE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC;KACvE,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE,QAAQ;IAClB,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,SAAS,CAAC;IAChE,OAAO,EAAE,QAAQ;CAClB,CAAC,CAAC;AAEH,yGAAyG;AACzG,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,MAAM,CAAC;CAC3D,CAAC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
/**
|
|
3
|
+
* mysql2 pool for Drizzle. Sessions run in UTC and utf8mb4 so timestamps and emoji round-trip
|
|
4
|
+
* unchanged regardless of server defaults.
|
|
5
|
+
*/
|
|
6
|
+
export function createMysqlPool(options) {
|
|
7
|
+
return mysql.createPool({
|
|
8
|
+
host: options.host,
|
|
9
|
+
port: options.port,
|
|
10
|
+
user: options.user,
|
|
11
|
+
password: options.password,
|
|
12
|
+
database: options.database,
|
|
13
|
+
connectionLimit: options.poolMax ?? 10,
|
|
14
|
+
waitForConnections: true,
|
|
15
|
+
enableKeepAlive: true,
|
|
16
|
+
timezone: 'Z',
|
|
17
|
+
charset: 'utf8mb4_unicode_ci',
|
|
18
|
+
dateStrings: false,
|
|
19
|
+
supportBigNumbers: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Knex config for migrations/seeds and query-builder use (same session settings as the pool). */
|
|
23
|
+
export function createKnexConfig(options) {
|
|
24
|
+
return {
|
|
25
|
+
client: 'mysql2',
|
|
26
|
+
connection: {
|
|
27
|
+
host: options.host,
|
|
28
|
+
port: options.port,
|
|
29
|
+
user: options.user,
|
|
30
|
+
password: options.password,
|
|
31
|
+
database: options.database,
|
|
32
|
+
timezone: 'Z',
|
|
33
|
+
charset: 'utf8mb4',
|
|
34
|
+
},
|
|
35
|
+
pool: { min: 0, max: options.poolMax ?? 10 },
|
|
36
|
+
migrations: { directory: options.migrationsDir ?? './migrations', tableName: 'knex_migrations', loadExtensions: ['.cjs', '.js'] },
|
|
37
|
+
seeds: { directory: options.seedsDir ?? './seeds', loadExtensions: ['.cjs', '.js'] },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Readiness probe helper. */
|
|
41
|
+
export async function pingMysql(pool) {
|
|
42
|
+
await pool.query('SELECT 1');
|
|
43
|
+
}
|
|
44
|
+
/** Escapes `%`, `_` and `\` for use inside a LIKE pattern. */
|
|
45
|
+
export function escapeLike(value) {
|
|
46
|
+
return value.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/database/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,gBAAgB,CAAC;AAWnC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAA+B;IAC7D,OAAO,KAAK,CAAC,UAAU,CAAC;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,eAAe,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;QACtC,kBAAkB,EAAE,IAAI;QACxB,eAAe,EAAE,IAAI;QACrB,QAAQ,EAAE,GAAG;QACb,OAAO,EAAE,oBAAoB;QAC7B,WAAW,EAAE,KAAK;QAClB,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAC;AACL,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,gBAAgB,CAAC,OAA+E;IAC9G,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,UAAU,EAAE;YACV,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,GAAG;YACb,OAAO,EAAE,SAAS;SACnB;QACD,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;QAC5C,UAAU,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,aAAa,IAAI,cAAc,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;QACjI,KAAK,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS,EAAE,cAAc,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;KACrF,CAAC;AACJ,CAAC;AAED,8BAA8B;AAC9B,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAgB;IAC9C,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAC/B,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { hasAllPermissions, hasPermission, hasRole } from '../access/index.js';
|
|
2
|
+
import { safeEqual } from '../common/crypto.js';
|
|
3
|
+
import { errorResponse } from '../common/response.js';
|
|
4
|
+
import { CONTEXT_HEADERS, verifyIdentityHeaders } from '../identity/index.js';
|
|
5
|
+
function resolveIdentity(req, options) {
|
|
6
|
+
const result = verifyIdentityHeaders(req.headers, options.secret, { maxSkewMs: options.maxSkewMs });
|
|
7
|
+
if (result.status === 'authenticated') {
|
|
8
|
+
req.user = result.user;
|
|
9
|
+
return 'ok';
|
|
10
|
+
}
|
|
11
|
+
if (result.status === 'invalid') {
|
|
12
|
+
options.logger?.warn('Rejected forged or stale gateway identity', { reason: result.reason, path: req.path });
|
|
13
|
+
return 'invalid';
|
|
14
|
+
}
|
|
15
|
+
return 'anonymous';
|
|
16
|
+
}
|
|
17
|
+
/** Requires a valid gateway-signed identity; 401 otherwise. */
|
|
18
|
+
export function authenticateFromGateway(options) {
|
|
19
|
+
return (req, res, next) => {
|
|
20
|
+
if (resolveIdentity(req, options) === 'ok')
|
|
21
|
+
return next();
|
|
22
|
+
res.status(401).json(errorResponse('Authentication required', undefined, 'UNAUTHENTICATED'));
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Attaches `req.user` when a valid identity is present; continues anonymously otherwise. */
|
|
26
|
+
export function optionalAuthFromGateway(options) {
|
|
27
|
+
return (req, res, next) => {
|
|
28
|
+
if (resolveIdentity(req, options) === 'invalid') {
|
|
29
|
+
res.status(401).json(errorResponse('Invalid identity', undefined, 'UNAUTHENTICATED'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function guard(check, message = 'You do not have permission to perform this action') {
|
|
36
|
+
return (req, res, next) => {
|
|
37
|
+
if (!req.user) {
|
|
38
|
+
res.status(401).json(errorResponse('Authentication required', undefined, 'UNAUTHENTICATED'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!check(req.user)) {
|
|
42
|
+
res.status(403).json(errorResponse(message, undefined, 'FORBIDDEN'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
next();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Allows callers holding any of `permissions`. */
|
|
49
|
+
export const requirePermission = (...permissions) => guard((u) => hasPermission(u, ...permissions));
|
|
50
|
+
/** Allows callers holding every one of `permissions`. */
|
|
51
|
+
export const requireAllPermissions = (...permissions) => guard((u) => hasAllPermissions(u, ...permissions));
|
|
52
|
+
/** Allows callers with any of `roles`. Prefer permissions; roles are for coarse gates only. */
|
|
53
|
+
export const requireRole = (...roles) => guard((u) => hasRole(u, ...roles), 'Insufficient role');
|
|
54
|
+
/** Guards `/internal/*` routes: requires the shared service secret header. */
|
|
55
|
+
export function requireInternalServiceSecret(secret) {
|
|
56
|
+
return (req, res, next) => {
|
|
57
|
+
const provided = req.headers[CONTEXT_HEADERS.INTERNAL_SECRET];
|
|
58
|
+
if (typeof provided === 'string' && secret && safeEqual(provided, secret))
|
|
59
|
+
return next();
|
|
60
|
+
res.status(401).json(errorResponse('Invalid internal service credentials', undefined, 'UNAUTHENTICATED'));
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/express/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAkB9E,SAAS,eAAe,CAAC,GAAY,EAAE,OAA2B;IAChE,MAAM,MAAM,GAAG,qBAAqB,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACpG,IAAI,MAAM,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;QACtC,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,2CAA2C,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7G,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,uBAAuB,CAAC,OAA2B;IACjE,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,IAAI;YAAE,OAAO,IAAI,EAAE,CAAC;QAC1D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,yBAAyB,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC/F,CAAC,CAAC;AACJ,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,uBAAuB,CAAC,OAA2B;IACjE,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;YAChD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,kBAAkB,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC;YACtF,OAAO;QACT,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,KAAkC,EAAE,OAAO,GAAG,mDAAmD;IAC9G,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,yBAAyB,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC;YAC7F,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;YACrE,OAAO;QACT,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAG,WAAqB,EAAkB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC;AAE9H,yDAAyD;AACzD,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,WAAqB,EAAkB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC;AAEtI,+FAA+F;AAC/F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAG,KAAe,EAAkB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,mBAAmB,CAAC,CAAC;AAE3H,8EAA8E;AAC9E,MAAM,UAAU,4BAA4B,CAAC,MAAc;IACzD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;QAC9D,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,IAAI,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QACzF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,sCAAsC,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC5G,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { withTimeout } from '../common/async.js';
|
|
2
|
+
/** `GET /health` — liveness: the process is up. Never touches dependencies. */
|
|
3
|
+
export function livenessHandler(options) {
|
|
4
|
+
const startedAt = Date.now();
|
|
5
|
+
return (_req, res) => {
|
|
6
|
+
res.json({ status: 'ok', service: options.service, version: options.version, uptimeSec: Math.round((Date.now() - startedAt) / 1000) });
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** `GET /health/ready` — readiness: every dependency responds. 503 when any fails. */
|
|
10
|
+
export function readinessHandler(options) {
|
|
11
|
+
return async (_req, res) => {
|
|
12
|
+
const entries = Object.entries(options.checks ?? {});
|
|
13
|
+
const results = await Promise.all(entries.map(async ([name, check]) => {
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
try {
|
|
16
|
+
const detail = await withTimeout(Promise.resolve(check()), options.timeoutMs ?? 3000, () => new Error('timeout'));
|
|
17
|
+
return [name, { status: 'up', latencyMs: Date.now() - started, ...(detail && typeof detail === 'object' ? { detail } : {}) }];
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return [name, { status: 'down', latencyMs: Date.now() - started, error: error.message }];
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
const healthy = results.every(([, r]) => r.status === 'up');
|
|
24
|
+
res.status(healthy ? 200 : 503).json({ status: healthy ? 'ok' : 'degraded', service: options.service, checks: Object.fromEntries(results) });
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../../src/express/health.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAYjD,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,OAAsB;IACpD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,OAAO,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QACtC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IACzI,CAAC,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,gBAAgB,CAAC,OAAsB;IACrD,OAAO,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,EAAE;QAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE;YACpE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;gBAClH,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAU,CAAC;YACzI,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,KAAK,EAAG,KAAe,CAAC,OAAO,EAAE,CAAU,CAAC;YAC/G,CAAC;QACH,CAAC,CAAC,CAAC,CAAC;QACJ,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;QAC5D,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/I,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/express/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isAppError, publicMessage } from '../common/errors.js';
|
|
2
|
+
import { newId } from '../common/id.js';
|
|
3
|
+
import { errorResponse } from '../common/response.js';
|
|
4
|
+
import { CONTEXT_HEADERS } from '../identity/index.js';
|
|
5
|
+
import { zodIssuesToErrors } from '../validation/index.js';
|
|
6
|
+
/** Ensures every request carries `x-correlation-id` (reused from the gateway when present). */
|
|
7
|
+
export function correlationMiddleware() {
|
|
8
|
+
return (req, res, next) => {
|
|
9
|
+
const incoming = req.headers[CONTEXT_HEADERS.CORRELATION_ID];
|
|
10
|
+
const id = typeof incoming === 'string' && incoming.length <= 128 ? incoming : newId();
|
|
11
|
+
req.headers[CONTEXT_HEADERS.CORRELATION_ID] = id;
|
|
12
|
+
res.setHeader(CONTEXT_HEADERS.CORRELATION_ID, id);
|
|
13
|
+
next();
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function correlationId(req) {
|
|
17
|
+
const value = req.headers[CONTEXT_HEADERS.CORRELATION_ID];
|
|
18
|
+
return typeof value === 'string' ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Real client IP: gateway-forwarded header first, then Express's `req.ip`. */
|
|
21
|
+
export function clientIp(req) {
|
|
22
|
+
const forwarded = req.headers[CONTEXT_HEADERS.CLIENT_IP];
|
|
23
|
+
if (typeof forwarded === 'string' && forwarded)
|
|
24
|
+
return forwarded;
|
|
25
|
+
return req.ip ?? null;
|
|
26
|
+
}
|
|
27
|
+
export function clientTimezone(req) {
|
|
28
|
+
const tz = req.headers[CONTEXT_HEADERS.TIMEZONE];
|
|
29
|
+
return typeof tz === 'string' && tz ? tz : null;
|
|
30
|
+
}
|
|
31
|
+
export function anonId(req) {
|
|
32
|
+
const id = req.headers[CONTEXT_HEADERS.ANON_ID];
|
|
33
|
+
return typeof id === 'string' && id ? id : null;
|
|
34
|
+
}
|
|
35
|
+
/** Validates and replaces `req[part]` with the parsed value; 422 with field errors on failure. */
|
|
36
|
+
export function validate(schema, part = 'body') {
|
|
37
|
+
return (req, res, next) => {
|
|
38
|
+
const result = schema.safeParse(req[part]);
|
|
39
|
+
if (!result.success) {
|
|
40
|
+
res.status(422).json(errorResponse('Validation failed', zodIssuesToErrors(result.error), 'VALIDATION_FAILED'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Express 5 exposes req.query as a getter; redefine to store the parsed value.
|
|
44
|
+
Object.defineProperty(req, part, { value: result.data, writable: true, configurable: true, enumerable: true });
|
|
45
|
+
next();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Wraps an async handler so rejections reach the error middleware (needed on Express 4). */
|
|
49
|
+
export function asyncHandler(fn) {
|
|
50
|
+
return (req, res, next) => {
|
|
51
|
+
fn(req, res, next).catch(next);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Maps AppErrors to their status; everything else becomes a logged 500 with a generic message. */
|
|
55
|
+
export function errorHandler(logger) {
|
|
56
|
+
return (err, req, res, _next) => {
|
|
57
|
+
if (res.headersSent)
|
|
58
|
+
return;
|
|
59
|
+
const bodyError = err;
|
|
60
|
+
if (bodyError?.type === 'entity.parse.failed') {
|
|
61
|
+
res.status(400).json(errorResponse('Malformed JSON body', undefined, 'BAD_REQUEST'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (bodyError?.type === 'entity.too.large') {
|
|
65
|
+
res.status(413).json(errorResponse('Request body too large', undefined, 'PAYLOAD_TOO_LARGE'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isAppError(err)) {
|
|
69
|
+
if (err.statusCode >= 500)
|
|
70
|
+
logger?.error(err.message, { path: req.path, code: err.code, correlationId: correlationId(req) });
|
|
71
|
+
res.status(err.statusCode).json(errorResponse(publicMessage(err), err.errors, err.code));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
75
|
+
logger?.error('Unhandled error', { error: error.message, stack: error.stack, path: req.path, correlationId: correlationId(req) });
|
|
76
|
+
res.status(500).json(errorResponse('Internal server error', undefined, 'INTERNAL_ERROR'));
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function notFoundHandler() {
|
|
80
|
+
return (_req, res) => {
|
|
81
|
+
res.status(404).json(errorResponse('Resource not found', undefined, 'NOT_FOUND'));
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../src/express/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AppError, ServiceUnavailableError } from '../common/errors.js';
|
|
2
|
+
import { sleep } from '../common/async.js';
|
|
3
|
+
import { CONTEXT_HEADERS } from '../identity/index.js';
|
|
4
|
+
import { silentLogger } from '../logging/index.js';
|
|
5
|
+
export * from './safe-fetch.js';
|
|
6
|
+
const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
7
|
+
const IDEMPOTENT = new Set(['GET', 'PUT', 'DELETE']);
|
|
8
|
+
/**
|
|
9
|
+
* Typed client for `/internal/*` calls between services. Unwraps the `{ success, data }`
|
|
10
|
+
* envelope, maps upstream errors to AppErrors with the same status, and turns network failures
|
|
11
|
+
* and timeouts into 503s.
|
|
12
|
+
*/
|
|
13
|
+
export function createServiceClient(options) {
|
|
14
|
+
const logger = options.logger ?? silentLogger;
|
|
15
|
+
const base = options.baseUrl.replace(/\/+$/, '');
|
|
16
|
+
async function request(method, path, opts = {}) {
|
|
17
|
+
const url = new URL(base + path);
|
|
18
|
+
for (const [key, value] of Object.entries(opts.query ?? {}))
|
|
19
|
+
if (value !== undefined)
|
|
20
|
+
url.searchParams.set(key, String(value));
|
|
21
|
+
const retries = IDEMPOTENT.has(method) ? options.retries ?? 2 : 0;
|
|
22
|
+
for (let attempt = 0;; attempt += 1) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? options.timeoutMs ?? 5000);
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method,
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
headers: {
|
|
30
|
+
accept: 'application/json',
|
|
31
|
+
...(opts.body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
32
|
+
[CONTEXT_HEADERS.INTERNAL_SECRET]: options.internalSecret,
|
|
33
|
+
[CONTEXT_HEADERS.CALLER_SERVICE]: options.callerService,
|
|
34
|
+
...(opts.correlationId ? { [CONTEXT_HEADERS.CORRELATION_ID]: opts.correlationId } : {}),
|
|
35
|
+
...opts.headers,
|
|
36
|
+
},
|
|
37
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
38
|
+
});
|
|
39
|
+
if (RETRYABLE_STATUS.has(response.status) && attempt < retries) {
|
|
40
|
+
await sleep(200 * 2 ** attempt);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
const payload = text ? JSON.parse(text) : { success: response.ok, message: '' };
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new AppError(response.status, payload.message || `Upstream error ${response.status}`, { code: payload.code, errors: payload.errors });
|
|
47
|
+
}
|
|
48
|
+
return (payload.data ?? payload);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof AppError)
|
|
52
|
+
throw error;
|
|
53
|
+
if (attempt < retries) {
|
|
54
|
+
await sleep(200 * 2 ** attempt);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
logger.warn('Internal call failed', { method, url: url.pathname, error: error.message });
|
|
58
|
+
throw new ServiceUnavailableError('Upstream service unavailable', { cause: error });
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
get: (path, opts) => request('GET', path, opts),
|
|
67
|
+
post: (path, opts) => request('POST', path, opts),
|
|
68
|
+
put: (path, opts) => request('PUT', path, opts),
|
|
69
|
+
patch: (path, opts) => request('PATCH', path, opts),
|
|
70
|
+
delete: (path, opts) => request('DELETE', path, opts),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":"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"}
|