@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.
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.