@magield/node 0.0.0-stage → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/dist/breaker.d.ts +41 -0
- package/dist/breaker.js +91 -0
- package/dist/client.d.ts +100 -0
- package/dist/client.js +407 -0
- package/dist/decisions.d.ts +17 -0
- package/dist/decisions.js +60 -0
- package/dist/express.d.ts +69 -0
- package/dist/express.js +59 -0
- package/dist/fastify.d.ts +39 -0
- package/dist/fastify.js +29 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/nest.d.ts +45 -0
- package/dist/nest.js +39 -0
- package/dist/password.d.ts +25 -0
- package/dist/password.js +30 -0
- package/dist/types.d.ts +273 -0
- package/dist/types.js +16 -0
- package/package.json +39 -3
- package/README.md +0 -3
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an integrator writes and reads.
|
|
3
|
+
*
|
|
4
|
+
* None of it is protobuf. That is the point of this file: the generated types
|
|
5
|
+
* are complete by definition, so every message has to be built whole, and
|
|
6
|
+
* `fromPartial` — the thing that fills the zeroes for you — is constrained by an
|
|
7
|
+
* `Exact<>` type that only infers correctly from an object literal written at
|
|
8
|
+
* the call site. Every hand-rolled client in this repository works around that
|
|
9
|
+
* the same way, by building the literal inline and passing a finished message
|
|
10
|
+
* around. An integrator should never meet it.
|
|
11
|
+
*
|
|
12
|
+
* So the shapes here are ordinary TypeScript. Strings are strings, the token is
|
|
13
|
+
* a string, `ip` is where somebody would look for it, and absent means absent.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The verdict, in the vocabulary of `docs/api.md` §5.
|
|
17
|
+
*
|
|
18
|
+
* Lower case and unprefixed, because that is the published contract and because
|
|
19
|
+
* `DECISION_ALLOW_FLAGGED` is not a thing to read forty times in a switch. The
|
|
20
|
+
* mapping to the wire enum is in `decisions.ts` and is the only place either
|
|
21
|
+
* spelling appears.
|
|
22
|
+
*/
|
|
23
|
+
export type DecisionName = 'allow' | 'allow_flagged' | 'challenge' | 'step_up' | 'throttle' | 'deny' | 'ban';
|
|
24
|
+
/**
|
|
25
|
+
* The eleven outcomes, in the short form `docs/api.md` §5 publishes.
|
|
26
|
+
*
|
|
27
|
+
* `confirmed_good` and `confirmed_fraud` are the two that matter most: they are
|
|
28
|
+
* what turns a decision into a labelled example, and without them there is no
|
|
29
|
+
* false-positive rate to measure and nothing to calibrate against.
|
|
30
|
+
*/
|
|
31
|
+
export type OutcomeName = 'login_success' | 'login_failure' | 'mfa_success' | 'mfa_failure' | 'confirmed_good' | 'confirmed_fraud' | 'chargeback' | 'manual_review_good' | 'manual_review_bad' | 'user_complaint' | 'support_unblock';
|
|
32
|
+
/**
|
|
33
|
+
* Business identifiers, all optional.
|
|
34
|
+
*
|
|
35
|
+
* Send what the action actually has. A password reset knows an email and no
|
|
36
|
+
* username; a checkout knows a user id and no password. Nothing here is
|
|
37
|
+
* required and nothing is penalised for being absent — the engine scores what
|
|
38
|
+
* it is given.
|
|
39
|
+
*
|
|
40
|
+
* These are hashed with the tenant's salt before they reach any datastore, so
|
|
41
|
+
* a username never lands in a database, a log line or a metric label.
|
|
42
|
+
*/
|
|
43
|
+
export interface Identifiers {
|
|
44
|
+
readonly username?: string;
|
|
45
|
+
readonly userId?: string;
|
|
46
|
+
readonly email?: string;
|
|
47
|
+
readonly phone?: string;
|
|
48
|
+
readonly sessionId?: string;
|
|
49
|
+
readonly apiKeyId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The first five hex characters of the password's SHA-1, and nothing more.
|
|
52
|
+
*
|
|
53
|
+
* Use `passwordPrefix()` to compute it. It exists so a limit can notice one
|
|
54
|
+
* password being tried against many accounts without anyone holding the
|
|
55
|
+
* password — five characters is roughly a millionth of the space, which
|
|
56
|
+
* identifies a spraying campaign and identifies no individual.
|
|
57
|
+
*/
|
|
58
|
+
readonly passwordSha1Prefix?: string;
|
|
59
|
+
readonly custom?: Readonly<Record<string, string>>;
|
|
60
|
+
}
|
|
61
|
+
/** What the socket revealed, when something in front of you observed it. */
|
|
62
|
+
export interface NetworkContext {
|
|
63
|
+
readonly ip?: string;
|
|
64
|
+
readonly forwardedFor?: string;
|
|
65
|
+
readonly ja3?: string;
|
|
66
|
+
readonly ja4?: string;
|
|
67
|
+
readonly userAgent?: string;
|
|
68
|
+
readonly headerOrder?: readonly string[];
|
|
69
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
70
|
+
}
|
|
71
|
+
export interface EvaluateInput {
|
|
72
|
+
/** `'login'`, `'signup'`, `'checkout'` — yours to name. */
|
|
73
|
+
readonly action: string;
|
|
74
|
+
/**
|
|
75
|
+
* The sealed string the web SDK returned.
|
|
76
|
+
*
|
|
77
|
+
* Absent is fine and is not an error: a missing token on a protected action
|
|
78
|
+
* is evidence (`TOK_ABSENT`) and produces a normal scored response. A page
|
|
79
|
+
* with JavaScript disabled is a real visitor, and refusing to decide about
|
|
80
|
+
* them would be the wrong answer to a question that has one.
|
|
81
|
+
*/
|
|
82
|
+
readonly token?: string;
|
|
83
|
+
readonly keys?: Identifiers;
|
|
84
|
+
/** The caller's address. Convenience for `network.ip`, which is where it goes. */
|
|
85
|
+
readonly ip?: string;
|
|
86
|
+
readonly network?: NetworkContext;
|
|
87
|
+
/** Overrides the client's `appId` for this call. Rarely needed. */
|
|
88
|
+
readonly appId?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Your own idempotency key.
|
|
91
|
+
*
|
|
92
|
+
* Generated when omitted. Supplying one lets you retry across process
|
|
93
|
+
* boundaries and get the original decision back rather than a second one —
|
|
94
|
+
* the gateway holds it for sixty seconds.
|
|
95
|
+
*/
|
|
96
|
+
readonly requestId?: string;
|
|
97
|
+
/** A solved challenge, on the retry after one was issued. */
|
|
98
|
+
readonly solution?: ChallengeSolution;
|
|
99
|
+
/** Per-call deadline, overriding the client default. */
|
|
100
|
+
readonly deadlineMs?: number;
|
|
101
|
+
/** Ask for the `detail` map on each reason. Costs response size. */
|
|
102
|
+
readonly verboseReasons?: boolean;
|
|
103
|
+
/** Free-form context the rules can read. `origin` is the one that matters. */
|
|
104
|
+
readonly context?: Readonly<Record<string, string>>;
|
|
105
|
+
}
|
|
106
|
+
export interface ChallengeSolution {
|
|
107
|
+
readonly blob: string;
|
|
108
|
+
readonly nonce: string;
|
|
109
|
+
readonly solveMs: number;
|
|
110
|
+
}
|
|
111
|
+
/** A challenge to hand to the page. Opaque, sealed, and bound to this device. */
|
|
112
|
+
export interface ChallengeOffer {
|
|
113
|
+
readonly blob: string;
|
|
114
|
+
readonly difficulty: number;
|
|
115
|
+
readonly subtasks: number;
|
|
116
|
+
readonly algorithm: string;
|
|
117
|
+
readonly expectedMs: number;
|
|
118
|
+
readonly ttlMs: number;
|
|
119
|
+
}
|
|
120
|
+
export interface Reason {
|
|
121
|
+
readonly code: string;
|
|
122
|
+
readonly dimension: string;
|
|
123
|
+
/** Negative weights exonerate. */
|
|
124
|
+
readonly weight: number;
|
|
125
|
+
readonly confidence: number;
|
|
126
|
+
readonly message: string;
|
|
127
|
+
readonly detail?: Readonly<Record<string, string>>;
|
|
128
|
+
}
|
|
129
|
+
export interface LimitState {
|
|
130
|
+
readonly id: string;
|
|
131
|
+
readonly limit: number;
|
|
132
|
+
readonly remaining: number;
|
|
133
|
+
readonly resetAtMs: number;
|
|
134
|
+
/** This limit is the reason for the decision, or would have been. */
|
|
135
|
+
readonly exceeded: boolean;
|
|
136
|
+
/** Evaluated and not enforced, so it can be tuned before it bites. */
|
|
137
|
+
readonly shadow: boolean;
|
|
138
|
+
/** Enough of the key to say which bucket, never the raw identifier. */
|
|
139
|
+
readonly keyPreview: string;
|
|
140
|
+
}
|
|
141
|
+
/** What the engine would have done if the mode had let it. */
|
|
142
|
+
export interface WouldHave {
|
|
143
|
+
readonly decision: DecisionName;
|
|
144
|
+
readonly score: number;
|
|
145
|
+
}
|
|
146
|
+
interface VerdictBase {
|
|
147
|
+
/**
|
|
148
|
+
* The id this decision is recorded under.
|
|
149
|
+
*
|
|
150
|
+
* Carry it into `feedback`. Empty only on a fail-open verdict, where there is
|
|
151
|
+
* no decision to join anything to.
|
|
152
|
+
*/
|
|
153
|
+
readonly evaluationId: string;
|
|
154
|
+
readonly score: number;
|
|
155
|
+
readonly confidence: number;
|
|
156
|
+
readonly reasons: readonly Reason[];
|
|
157
|
+
readonly subscores: Readonly<Record<string, number>>;
|
|
158
|
+
readonly limits: readonly LimitState[];
|
|
159
|
+
/** A stage failed open, so this decision was made with less than usual. */
|
|
160
|
+
readonly degraded: boolean;
|
|
161
|
+
/** Present when the mode softened the decision. The onboarding signal. */
|
|
162
|
+
readonly wouldHave?: WouldHave;
|
|
163
|
+
readonly deviceId: string;
|
|
164
|
+
readonly modeApplied: string;
|
|
165
|
+
/**
|
|
166
|
+
* Tells the engine what happened next.
|
|
167
|
+
*
|
|
168
|
+
* On the verdict rather than on the client, so the `evaluationId` cannot be
|
|
169
|
+
* dropped. Dropping it is a silent, structural failure: the outcome still
|
|
170
|
+
* arrives, still clears the limit counters a real user tripped with their own
|
|
171
|
+
* typos, and still looks like it worked — but it can never be joined to the
|
|
172
|
+
* decision it describes, so the false-positive rate it exists to measure
|
|
173
|
+
* reads a permanent zero. That was a real bug in this product's own demo, and
|
|
174
|
+
* nobody noticed until somebody went looking for the number.
|
|
175
|
+
*
|
|
176
|
+
* Fire and forget. A sign-in that already worked must not fail because a
|
|
177
|
+
* bookkeeping call did.
|
|
178
|
+
*/
|
|
179
|
+
reportOutcome(outcome: OutcomeName, extra?: OutcomeDetails): Promise<void>;
|
|
180
|
+
}
|
|
181
|
+
export interface OutcomeDetails {
|
|
182
|
+
readonly note?: string;
|
|
183
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
184
|
+
readonly occurredAtMs?: number;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The verdict, discriminated on the decision.
|
|
188
|
+
*
|
|
189
|
+
* `decision === 'challenge'` narrows `challenge` to defined, and nothing else
|
|
190
|
+
* does. That is not a convenience — it is an invariant the gateway guarantees
|
|
191
|
+
* by downgrading to `allow_flagged` when it cannot issue one, so encoding it in
|
|
192
|
+
* the type means a partner cannot write the branch that handles a challenge
|
|
193
|
+
* that is not there.
|
|
194
|
+
*
|
|
195
|
+
* `retryAfterMs` is narrowed the same way: it is zero on every decision but
|
|
196
|
+
* `throttle`, so only that member carries it.
|
|
197
|
+
*/
|
|
198
|
+
export type Verdict = (VerdictBase & {
|
|
199
|
+
readonly decision: 'challenge';
|
|
200
|
+
readonly challenge: ChallengeOffer;
|
|
201
|
+
}) | (VerdictBase & {
|
|
202
|
+
readonly decision: 'throttle';
|
|
203
|
+
readonly retryAfterMs: number;
|
|
204
|
+
}) | (VerdictBase & {
|
|
205
|
+
readonly decision: 'allow' | 'allow_flagged' | 'step_up' | 'deny' | 'ban';
|
|
206
|
+
});
|
|
207
|
+
export interface FeedbackInput {
|
|
208
|
+
/**
|
|
209
|
+
* The decision this outcome is about. Either this or `requestId`.
|
|
210
|
+
*
|
|
211
|
+
* Prefer `verdict.reportOutcome`, which carries it for you. Use this when
|
|
212
|
+
* the verdict is still in hand.
|
|
213
|
+
*/
|
|
214
|
+
readonly evaluationId?: string;
|
|
215
|
+
/**
|
|
216
|
+
* Your own reference for the request, as sent on the Evaluate.
|
|
217
|
+
*
|
|
218
|
+
* This is the one to use for a label that arrives later — a chargeback, a
|
|
219
|
+
* manual review, an account closed for abuse. The alternative is keeping our
|
|
220
|
+
* `evaluationId` in your database on your own retention, and that is the
|
|
221
|
+
* step integrations skip; when it is skipped the outcome still arrives,
|
|
222
|
+
* still clears the limits, still returns 200, and joins to nothing for ever.
|
|
223
|
+
*
|
|
224
|
+
* So send the reference you already have. Pass your transaction's primary
|
|
225
|
+
* key as `requestId` on `evaluate` — it doubles as the idempotency key the
|
|
226
|
+
* field is named for — and send the same value back whenever the verdict is
|
|
227
|
+
* reached. Nothing of ours needs storing anywhere.
|
|
228
|
+
*/
|
|
229
|
+
readonly requestId?: string;
|
|
230
|
+
readonly outcome: OutcomeName;
|
|
231
|
+
/**
|
|
232
|
+
* The action and identifiers the decision was about.
|
|
233
|
+
*
|
|
234
|
+
* Sent again rather than looked up, so reporting an outcome needs no
|
|
235
|
+
* server-side state between the two calls. They also drive `resetOn`: a
|
|
236
|
+
* limit a real user tripped is cleared by the success that proves they were
|
|
237
|
+
* entitled, and without these the reset is a silent no-op.
|
|
238
|
+
*/
|
|
239
|
+
readonly action?: string;
|
|
240
|
+
readonly keys?: Identifiers;
|
|
241
|
+
readonly note?: string;
|
|
242
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
243
|
+
readonly occurredAtMs?: number;
|
|
244
|
+
}
|
|
245
|
+
export interface TrustDeviceInput {
|
|
246
|
+
readonly deviceId: string;
|
|
247
|
+
readonly publicKey: string;
|
|
248
|
+
readonly appId?: string;
|
|
249
|
+
/** What justified the enrolment — a password, a second factor, a session. */
|
|
250
|
+
readonly authContext?: string;
|
|
251
|
+
}
|
|
252
|
+
export interface TrustedDevice {
|
|
253
|
+
readonly deviceId: string;
|
|
254
|
+
readonly state: string;
|
|
255
|
+
readonly trustedAtMs: number;
|
|
256
|
+
readonly lastSeenMs: number;
|
|
257
|
+
}
|
|
258
|
+
export interface LimitCheckInput {
|
|
259
|
+
readonly action: string;
|
|
260
|
+
readonly keys?: Identifiers;
|
|
261
|
+
readonly ip?: string;
|
|
262
|
+
readonly appId?: string;
|
|
263
|
+
/** Check without consuming budget. */
|
|
264
|
+
readonly peek?: boolean;
|
|
265
|
+
}
|
|
266
|
+
export interface LimitVerdict {
|
|
267
|
+
readonly allowed: boolean;
|
|
268
|
+
readonly decision: DecisionName;
|
|
269
|
+
readonly retryAfterMs: number;
|
|
270
|
+
readonly limits: readonly LimitState[];
|
|
271
|
+
}
|
|
272
|
+
export {};
|
|
273
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an integrator writes and reads.
|
|
3
|
+
*
|
|
4
|
+
* None of it is protobuf. That is the point of this file: the generated types
|
|
5
|
+
* are complete by definition, so every message has to be built whole, and
|
|
6
|
+
* `fromPartial` — the thing that fills the zeroes for you — is constrained by an
|
|
7
|
+
* `Exact<>` type that only infers correctly from an object literal written at
|
|
8
|
+
* the call site. Every hand-rolled client in this repository works around that
|
|
9
|
+
* the same way, by building the literal inline and passing a finished message
|
|
10
|
+
* around. An integrator should never meet it.
|
|
11
|
+
*
|
|
12
|
+
* So the shapes here are ordinary TypeScript. Strings are strings, the token is
|
|
13
|
+
* a string, `ip` is where somebody would look for it, and absent means absent.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magield/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Magic Shield backend SDK",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"!**/*.map",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org/"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/index.js",
|
|
18
|
+
"./express": "./dist/express.js",
|
|
19
|
+
"./fastify": "./dist/fastify.js",
|
|
20
|
+
"./nest": "./dist/nest.js"
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@grpc/grpc-js": "^1.12.4",
|
|
26
|
+
"@magield/proto": "0.6.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.2",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"vitest": "^2.1.8",
|
|
32
|
+
"@magield/gateway": "0.6.1",
|
|
33
|
+
"@magield/storage": "0.6.1"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"typecheck:tests": "tsc -p tsconfig.test.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"clean": "rm -rf dist .turbo"
|
|
41
|
+
}
|
|
6
42
|
}
|
package/README.md
DELETED