@owlmeans/server-api 0.1.18-rc.4 → 0.1.18-rc.41
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 +202 -46
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-api/SKILL.md +104 -22
- package/build/errors.d.ts +12 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +16 -0
- package/build/errors.js.map +1 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +10 -1
- package/build/helper.js.map +1 -1
- package/build/hold.d.ts +52 -0
- package/build/hold.d.ts.map +1 -0
- package/build/hold.js +56 -0
- package/build/hold.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/protocol.d.ts +45 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +86 -0
- package/build/protocol.js.map +1 -0
- package/build/server.d.ts.map +1 -1
- package/build/server.js +9 -13
- package/build/server.js.map +1 -1
- package/build/types.d.ts +10 -2
- package/build/types.d.ts.map +1 -1
- package/build/utils/error.d.ts +64 -1
- package/build/utils/error.d.ts.map +1 -1
- package/build/utils/error.js +160 -8
- package/build/utils/error.js.map +1 -1
- package/build/utils/guards.d.ts.map +1 -1
- package/build/utils/guards.js +11 -15
- package/build/utils/guards.js.map +1 -1
- package/build/utils/payload.d.ts.map +1 -1
- package/build/utils/payload.js +6 -2
- package/build/utils/payload.js.map +1 -1
- package/build/utils/server.d.ts +3 -2
- package/build/utils/server.d.ts.map +1 -1
- package/build/utils/server.js +5 -6
- package/build/utils/server.js.map +1 -1
- package/package.json +13 -11
- package/src/errors.ts +18 -0
- package/src/helper.ts +10 -1
- package/src/hold.ts +84 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +185 -0
- package/src/server.ts +36 -45
- package/src/types.ts +15 -4
- package/src/utils/error.ts +198 -10
- package/src/utils/guards.ts +10 -17
- package/src/utils/payload.ts +6 -2
- package/src/utils/server.ts +8 -8
- package/tests/error-status.spec.ts +290 -0
- package/tests/handlers.spec.ts +83 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +1 -0
- package/build/.gitkeep +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import Fastify from 'fastify'
|
|
3
|
+
import type { FastifyInstance } from 'fastify'
|
|
4
|
+
import { AuthForbidden, AuthorizationError } from '@owlmeans/auth'
|
|
5
|
+
import { ResilientError, SEPARATOR } from '@owlmeans/error'
|
|
6
|
+
import { provideResponse } from '@owlmeans/entrypoint'
|
|
7
|
+
import { errorExposure, errorStatus, handleError, INCIDENT_ID_HEADER } from '../src/utils/error.js'
|
|
8
|
+
import { executeResponse } from '../src/utils/payload.js'
|
|
9
|
+
import { AccessError, AuthFailedError } from '../src/errors.js'
|
|
10
|
+
|
|
11
|
+
class OutOfCredit extends ResilientError {
|
|
12
|
+
public static override typeName = 'ServerApiSpecOutOfCredit'
|
|
13
|
+
public static httpStatus = 402
|
|
14
|
+
|
|
15
|
+
constructor(message: string = 'error') {
|
|
16
|
+
super(OutOfCredit.typeName, `out-of-credit:${message}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Inherits 402 without redeclaring it. */
|
|
21
|
+
class OutOfCreditForStory extends OutOfCredit {
|
|
22
|
+
public static override typeName = 'ServerApiSpecOutOfCreditForStory'
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super('story')
|
|
26
|
+
this.type = OutOfCreditForStory.typeName
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Redeclares its parent's status to a conflict. */
|
|
31
|
+
class Busy extends OutOfCredit {
|
|
32
|
+
public static override typeName = 'ServerApiSpecBusy'
|
|
33
|
+
public static override httpStatus = 409
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
super('busy')
|
|
37
|
+
this.type = Busy.typeName
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** An auth refusal that also declares a status: the auth mapping still wins. */
|
|
42
|
+
class DeclaringForbidden extends AuthForbidden {
|
|
43
|
+
public static override typeName = 'ServerApiSpecDeclaringForbidden'
|
|
44
|
+
public static httpStatus = 402
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
super('declaring')
|
|
48
|
+
this.type = DeclaringForbidden.typeName
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class TemporarilyUnavailable extends ResilientError {
|
|
53
|
+
public static override typeName = 'ServerApiSpecTemporarilyUnavailable'
|
|
54
|
+
public static httpStatus = 503
|
|
55
|
+
public static allowServerErrorStatus = true
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
super(TemporarilyUnavailable.typeName, 'temporarily-unavailable')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class Throttled extends ResilientError {
|
|
63
|
+
public static override typeName = 'ServerApiSpecThrottled'
|
|
64
|
+
public static httpStatus = 429
|
|
65
|
+
public readonly retryAfter = 17.2
|
|
66
|
+
|
|
67
|
+
constructor() {
|
|
68
|
+
super(Throttled.typeName, 'throttled')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const declaring = (typeName: string, httpStatus: unknown) => {
|
|
73
|
+
class Declaring extends ResilientError {
|
|
74
|
+
public static override typeName = typeName
|
|
75
|
+
public static httpStatus = httpStatus
|
|
76
|
+
|
|
77
|
+
constructor(message: string = 'error') {
|
|
78
|
+
super(typeName, message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Declaring
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A duplicate module copy of the error family — what a process loading `@owlmeans/error` through
|
|
86
|
+
* several `node_modules` paths holds: the same registered names, none of the imported classes.
|
|
87
|
+
*/
|
|
88
|
+
class ForeignResilientError extends Error {
|
|
89
|
+
public static typeName = 'ResilientError'
|
|
90
|
+
public type: string
|
|
91
|
+
|
|
92
|
+
constructor(type: string, message: string) {
|
|
93
|
+
super(message)
|
|
94
|
+
this.type = type
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class ForeignAuthFailedError extends ForeignResilientError {
|
|
99
|
+
public static override typeName = 'AuthFailedError'
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
super(ForeignAuthFailedError.typeName, 'api:auth:error')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class ForeignAuthorizationError extends ForeignResilientError {
|
|
107
|
+
public static override typeName = 'AuthorizationError'
|
|
108
|
+
|
|
109
|
+
constructor(message: string = 'error') {
|
|
110
|
+
super(ForeignAuthorizationError.typeName, `auth:authorization:${message}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class ForeignAuthForbidden extends ForeignAuthorizationError {
|
|
115
|
+
public static override typeName = 'ForbiddenAuthorizationError'
|
|
116
|
+
|
|
117
|
+
constructor(message: string = 'error') {
|
|
118
|
+
super(`forbidden:${message}`)
|
|
119
|
+
this.type = ForeignAuthForbidden.typeName
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A subclass whose own name embeds nothing of its parent's, like `EntitlementRefusal`. */
|
|
124
|
+
class ForeignEntitlementRefusal extends ForeignAuthForbidden {
|
|
125
|
+
public static override typeName = 'EntitlementRefusal'
|
|
126
|
+
|
|
127
|
+
constructor() {
|
|
128
|
+
super('entitlement')
|
|
129
|
+
this.type = ForeignEntitlementRefusal.typeName
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A declared refusal no registry of this process knows. */
|
|
134
|
+
class ForeignOutOfCredit extends ForeignResilientError {
|
|
135
|
+
public static override typeName = 'ServerApiSpecForeignOutOfCredit'
|
|
136
|
+
public static httpStatus = 402
|
|
137
|
+
|
|
138
|
+
constructor() {
|
|
139
|
+
super(ForeignOutOfCredit.typeName, 'out-of-credit:foreign')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const typed = (type: string) => Object.assign(new Error('typed'), { type })
|
|
144
|
+
|
|
145
|
+
ResilientError.registerErrorClass(OutOfCredit)
|
|
146
|
+
ResilientError.registerErrorClass(OutOfCreditForStory)
|
|
147
|
+
ResilientError.registerErrorClass(Busy)
|
|
148
|
+
ResilientError.registerErrorClass(DeclaringForbidden)
|
|
149
|
+
|
|
150
|
+
let server: FastifyInstance
|
|
151
|
+
let thrown: unknown
|
|
152
|
+
|
|
153
|
+
beforeAll(async () => {
|
|
154
|
+
server = Fastify()
|
|
155
|
+
server.get('/handle', async (_req, reply) => {
|
|
156
|
+
handleError(thrown as Error, reply)
|
|
157
|
+
return reply
|
|
158
|
+
})
|
|
159
|
+
server.get('/handle-development', async (_req, reply) => {
|
|
160
|
+
handleError(thrown as Error, reply, 'development')
|
|
161
|
+
return reply
|
|
162
|
+
})
|
|
163
|
+
server.get('/execute', async (_req, reply) => {
|
|
164
|
+
const response = provideResponse(reply)
|
|
165
|
+
response.reject(thrown as Error)
|
|
166
|
+
executeResponse(response, reply)
|
|
167
|
+
return reply
|
|
168
|
+
})
|
|
169
|
+
await server.ready()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
afterAll(async () => {
|
|
173
|
+
await server.close()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const answer = async (error: unknown, path: string = '/handle') => {
|
|
177
|
+
thrown = error
|
|
178
|
+
const reply = await server.inject({ method: 'GET', url: path })
|
|
179
|
+
return {
|
|
180
|
+
status: reply.statusCode,
|
|
181
|
+
body: reply.body,
|
|
182
|
+
incidentId: reply.headers[INCIDENT_ID_HEADER.toLowerCase()],
|
|
183
|
+
retryAfter: reply.headers['retry-after'],
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
describe('@owlmeans/server-api — production error exposure', () => {
|
|
188
|
+
test('returns only an incident id in production and correlates it with the header', async () => {
|
|
189
|
+
const response = await answer(new Error('private-marker'))
|
|
190
|
+
expect(response.body).toBe(response.incidentId)
|
|
191
|
+
expect(response.body).not.toContain('private-marker')
|
|
192
|
+
expect(response.body).not.toContain(SEPARATOR)
|
|
193
|
+
expect(response.body).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('includes stacks only under explicit development exposure and safely serializes SyntaxError', async () => {
|
|
197
|
+
const development = await answer(new Error('development-marker'), '/handle-development')
|
|
198
|
+
const [type, message, stack, incidentId] = development.body.split(SEPARATOR, 4)
|
|
199
|
+
expect(type).toBe(ResilientError.typeName)
|
|
200
|
+
expect(message).toBe('development-marker')
|
|
201
|
+
expect(stack).toContain('development-marker')
|
|
202
|
+
expect(incidentId).toBe(development.incidentId)
|
|
203
|
+
|
|
204
|
+
const syntax = await answer(new SyntaxError('bad wiring'))
|
|
205
|
+
expect(syntax.status).toBe(500)
|
|
206
|
+
expect(syntax.body).toBe(syntax.incidentId)
|
|
207
|
+
expect(syntax.body).not.toContain('bad wiring')
|
|
208
|
+
|
|
209
|
+
expect(errorExposure()).toBe('production')
|
|
210
|
+
expect(errorExposure({ http: { errors: { exposure: 'development' } } })).toBe('development')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('@owlmeans/server-api — handleError status', () => {
|
|
215
|
+
test('keeps the auth mappings: forbidden 403, unauthenticated 401', async () => {
|
|
216
|
+
expect((await answer(new AuthForbidden('x'))).status).toBe(403)
|
|
217
|
+
expect((await answer(new AccessError())).status).toBe(403)
|
|
218
|
+
expect((await answer(new AuthorizationError('x'))).status).toBe(401)
|
|
219
|
+
expect((await answer(new AuthFailedError())).status).toBe(401)
|
|
220
|
+
expect((await answer(new DeclaringForbidden())).status).toBe(403)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('honours a 4xx a class declares — inherited, redeclared, or after a marshal hop', async () => {
|
|
224
|
+
const refused = await answer(new OutOfCredit('create'))
|
|
225
|
+
expect(refused.status).toBe(402)
|
|
226
|
+
expect(refused.body).toBe(refused.incidentId)
|
|
227
|
+
|
|
228
|
+
expect((await answer(new OutOfCreditForStory())).status).toBe(402)
|
|
229
|
+
expect((await answer(new Busy())).status).toBe(409)
|
|
230
|
+
expect((await answer(ResilientError.marshal(new Busy()))).status).toBe(409)
|
|
231
|
+
expect((await answer(new ForeignOutOfCredit())).status).toBe(402)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('answers 500 for no declaration, or one outside the client-error range', async () => {
|
|
235
|
+
expect((await answer(new ResilientError('Plain', 'plain'))).status).toBe(500)
|
|
236
|
+
expect((await answer(new Error('boom'))).status).toBe(500)
|
|
237
|
+
for (const status of [500, 503, 200, 302, 399, 600, 402.5, '402', null]) {
|
|
238
|
+
const Declaring = declaring(`ServerApiSpecDeclaring${String(status)}`, status)
|
|
239
|
+
expect((await answer(new Declaring())).status).toBe(500)
|
|
240
|
+
}
|
|
241
|
+
for (const type of ['AuthFailedErrorX', 'NotAuthorizationError', 'Forbidden', 'ResilientError']) {
|
|
242
|
+
expect((await answer(typed(type))).status).toBe(500)
|
|
243
|
+
expect((await answer(new ForeignResilientError(type, 'near-miss'))).status).toBe(500)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('honours an explicitly exposed 5xx without opening arbitrary server statuses', async () => {
|
|
248
|
+
expect((await answer(new TemporarilyUnavailable())).status).toBe(503)
|
|
249
|
+
const Unexposed = declaring('ServerApiSpecUnexposed503', 503)
|
|
250
|
+
expect((await answer(new Unexposed())).status).toBe(500)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('emits Retry-After for a throttled response', async () => {
|
|
254
|
+
const response = await answer(new Throttled())
|
|
255
|
+
expect(response.status).toBe(429)
|
|
256
|
+
expect(response.retryAfter).toBe('18')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('executeResponse answers a rejected response with the same status', async () => {
|
|
260
|
+
expect((await answer(new OutOfCredit('create'), '/execute')).status).toBe(402)
|
|
261
|
+
expect((await answer(new AuthForbidden('x'), '/execute')).status).toBe(403)
|
|
262
|
+
expect((await answer(new ResilientError('Plain', 'plain'), '/execute')).status).toBe(500)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('@owlmeans/server-api — auth status across a rebuild and duplicate module copies', () => {
|
|
267
|
+
test('an AuthFailedError answers 401 as thrown and after a marshal round trip', async () => {
|
|
268
|
+
expect((await answer(new AuthFailedError())).status).toBe(401)
|
|
269
|
+
expect((await answer(ResilientError.marshal(new AuthFailedError()))).status).toBe(401)
|
|
270
|
+
expect(errorStatus(ResilientError.ensure(ResilientError.marshal(new AuthFailedError())))).toBe(401)
|
|
271
|
+
expect((await answer(new AuthFailedError(), '/execute')).status).toBe(401)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('a class from another module copy answers by its registered type name', async () => {
|
|
275
|
+
// The rebuild alone loses it — the regression this resolution exists for.
|
|
276
|
+
expect(errorStatus(ResilientError.ensure(new ForeignAuthFailedError()))).toBe(500)
|
|
277
|
+
expect((await answer(new ForeignAuthFailedError())).status).toBe(401)
|
|
278
|
+
expect((await answer(new ForeignAuthorizationError())).status).toBe(401)
|
|
279
|
+
expect((await answer(new ForeignAuthForbidden())).status).toBe(403)
|
|
280
|
+
expect((await answer(new ForeignAuthForbidden(), '/execute')).status).toBe(403)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('the family is read off the instance type or the constructor chain, 403 before 401', async () => {
|
|
284
|
+
expect((await answer(typed('AuthFailedError'))).status).toBe(401)
|
|
285
|
+
expect((await answer(typed('ForbiddenAuthorizationError'))).status).toBe(403)
|
|
286
|
+
expect((await answer(typed('AccessAuthFailedError'))).status).toBe(403)
|
|
287
|
+
// Its own type names nothing of the family; only the chain does.
|
|
288
|
+
expect((await answer(new ForeignEntitlementRefusal())).status).toBe(403)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { EntrypointProtocolDeclaration } from '@owlmeans/entrypoint'
|
|
3
|
+
import { handlers } from '../src/protocol.js'
|
|
4
|
+
import { HandlerMisconfiguredError } from '../src/errors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A protocol-bound implementation is run with the smallest binding context the transport needs —
|
|
8
|
+
* same pattern as `@owlmeans/server-auth-token`'s own `handlers.spec.ts`.
|
|
9
|
+
*/
|
|
10
|
+
const invoke = async (handler: any, context: any, req: any = {}): Promise<any> => {
|
|
11
|
+
const res: any = { resolve: (value: unknown) => { res.value = value }, reject: (e: Error) => { res.error = e } }
|
|
12
|
+
await handler.bind({ ref: { ctx: context } })(req, res)
|
|
13
|
+
if (res.error != null) throw res.error
|
|
14
|
+
|
|
15
|
+
return res.value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const protocolFor = (alias: string): EntrypointProtocolDeclaration =>
|
|
19
|
+
({ alias } as unknown as EntrypointProtocolDeclaration)
|
|
20
|
+
|
|
21
|
+
const context = { alias: 'test-context' } as any
|
|
22
|
+
const api = handlers<any>()
|
|
23
|
+
|
|
24
|
+
describe('@owlmeans/server-api — handlers() wrap-once tolerance', () => {
|
|
25
|
+
test('a plain function is wrapped once and called with the context', async () => {
|
|
26
|
+
const protocol = protocolFor('api.thing.plain')
|
|
27
|
+
const handler = api.request(protocol, async (_req: any, ctx: any) => ({ ok: true, ctx }))
|
|
28
|
+
|
|
29
|
+
const result = await invoke(handler, context, {})
|
|
30
|
+
expect(result).toEqual({ ok: true, ctx: context })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('a handler already bound to the SAME protocol is returned unchanged by body/params/request and still answers', async () => {
|
|
34
|
+
for (const method of ['body', 'params', 'request'] as const) {
|
|
35
|
+
const protocol = protocolFor(`api.thing.${method}`)
|
|
36
|
+
const bound = api.request(protocol, async () => ({ from: method }))
|
|
37
|
+
|
|
38
|
+
const wrapped = (api as any)[method](protocol, bound)
|
|
39
|
+
expect(wrapped).toBe(bound)
|
|
40
|
+
|
|
41
|
+
const result = await invoke(wrapped, context, {})
|
|
42
|
+
expect(result).toEqual({ from: method })
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('a handler bound to a DIFFERENT protocol answers with HandlerMisconfiguredError', async () => {
|
|
47
|
+
const protocolA = protocolFor('api.thing.mismatch-a')
|
|
48
|
+
const protocolB = protocolFor('api.thing.mismatch-b')
|
|
49
|
+
const boundToA = api.request(protocolA, async () => ({ from: 'a' }))
|
|
50
|
+
|
|
51
|
+
const wrapped = api.request(protocolB, boundToA as any)
|
|
52
|
+
expect(wrapped).not.toBe(boundToA)
|
|
53
|
+
await expect(invoke(wrapped, context, {})).rejects.toThrow(HandlerMisconfiguredError)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a value that is neither a function nor a bound handler answers with HandlerMisconfiguredError', async () => {
|
|
57
|
+
const protocol = protocolFor('api.thing.not-a-function')
|
|
58
|
+
const wrapped = api.request(protocol, { not: 'a function' } as any)
|
|
59
|
+
|
|
60
|
+
await expect(invoke(wrapped, context, {})).rejects.toThrow(HandlerMisconfiguredError)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('the double-wrap warning fires once per alias, not once per call', async () => {
|
|
64
|
+
const protocol = protocolFor('api.thing.warn-once')
|
|
65
|
+
const bound = api.request(protocol, async () => 'ok')
|
|
66
|
+
|
|
67
|
+
const warnings: unknown[][] = []
|
|
68
|
+
const original = console.warn
|
|
69
|
+
console.warn = (...args: unknown[]) => { warnings.push(args) }
|
|
70
|
+
try {
|
|
71
|
+
// Each call is a deliberate double wrap — the exact shape `tsc` rejects at the real call
|
|
72
|
+
// site (TS2345) — so it is exercised here through `any`, the same way `body`/`params`
|
|
73
|
+
// are reached dynamically above.
|
|
74
|
+
(api as any).request(protocol, bound);
|
|
75
|
+
(api as any).body(protocol, bound);
|
|
76
|
+
(api as any).params(protocol, bound)
|
|
77
|
+
} finally {
|
|
78
|
+
console.warn = original
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(warnings.length).toBe(1)
|
|
82
|
+
})
|
|
83
|
+
})
|
package/tsconfig.json
CHANGED
package/build/.gitkeep
DELETED
|
File without changes
|