@owlmeans/client-auth 0.1.18-rc.13 → 0.1.18-rc.15
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-auth/SKILL.md +7 -1
- package/build/login/credit.d.ts +5 -0
- package/build/login/credit.d.ts.map +1 -1
- package/build/login/credit.js +43 -9
- package/build/login/credit.js.map +1 -1
- package/build/login/flow.d.ts +20 -0
- package/build/login/flow.d.ts.map +1 -0
- package/build/login/flow.js +39 -0
- package/build/login/flow.js.map +1 -0
- package/build/login/i18n/be.json +1 -1
- package/build/login/i18n/de.json +1 -1
- package/build/login/i18n/en.json +1 -1
- package/build/login/i18n/es.json +1 -1
- package/build/login/i18n/pl.json +1 -1
- package/build/login/i18n/ru.json +1 -1
- package/build/login/i18n/uk.json +1 -1
- package/build/login/index.d.ts +1 -0
- package/build/login/index.d.ts.map +1 -1
- package/build/login/index.js +1 -0
- package/build/login/index.js.map +1 -1
- package/build/login/resume.d.ts +10 -0
- package/build/login/resume.d.ts.map +1 -1
- package/build/login/resume.js +22 -0
- package/build/login/resume.js.map +1 -1
- package/build/login/screen.d.ts +13 -0
- package/build/login/screen.d.ts.map +1 -1
- package/build/login/screen.js +27 -14
- package/build/login/screen.js.map +1 -1
- package/build/login/types.d.ts +9 -1
- package/build/login/types.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.js +4 -0
- package/build/manager/plugins/basic-ed25519.js.map +1 -1
- package/build/manager/plugins/methods.d.ts +0 -4
- package/build/manager/plugins/methods.d.ts.map +1 -1
- package/build/manager/plugins/methods.js +25 -6
- package/build/manager/plugins/methods.js.map +1 -1
- package/build/manager/plugins/re-captcha.d.ts.map +1 -1
- package/build/manager/plugins/re-captcha.js +3 -0
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/tunnel-consumer.d.ts.map +1 -1
- package/build/manager/plugins/tunnel-consumer.js +3 -0
- package/build/manager/plugins/tunnel-consumer.js.map +1 -1
- package/build/manager/plugins/types.d.ts +22 -0
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/login/credit.ts +47 -9
- package/src/login/flow.ts +44 -0
- package/src/login/i18n/be.json +1 -1
- package/src/login/i18n/de.json +1 -1
- package/src/login/i18n/en.json +1 -1
- package/src/login/i18n/es.json +1 -1
- package/src/login/i18n/pl.json +1 -1
- package/src/login/i18n/ru.json +1 -1
- package/src/login/i18n/uk.json +1 -1
- package/src/login/index.ts +1 -0
- package/src/login/resume.ts +23 -0
- package/src/login/screen.tsx +27 -13
- package/src/login/types.ts +9 -1
- package/src/manager/plugins/basic-ed25519.tsx +5 -0
- package/src/manager/plugins/methods.ts +29 -7
- package/src/manager/plugins/re-captcha.tsx +4 -0
- package/src/manager/plugins/tunnel-consumer.tsx +4 -0
- package/src/manager/plugins/types.ts +22 -0
- package/tests/login.spec.ts +115 -4
- package/build/.gitkeep +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FC } from 'react'
|
|
2
2
|
import type { LoginMethodEmphasis } from '@owlmeans/config'
|
|
3
|
+
import type { LoginMethodContext } from '../../login/types.js'
|
|
3
4
|
import type { AuthenticationControl, AuthenticationRenderer, AuthenticationRendererProps, } from '../components/authentication/types.js'
|
|
4
5
|
|
|
5
6
|
export interface AuthenticationPlugin extends Pick<
|
|
@@ -8,6 +9,15 @@ export interface AuthenticationPlugin extends Pick<
|
|
|
8
9
|
type: string
|
|
9
10
|
Implementation: PluginImplemnetation
|
|
10
11
|
Renderer?: AuthenticationRenderer
|
|
12
|
+
/**
|
|
13
|
+
* This plugin's `Implementation` cannot render without a `Renderer` a UI package supplies.
|
|
14
|
+
*
|
|
15
|
+
* It is declared rather than inferred because there is nothing to infer it from: a plugin that
|
|
16
|
+
* renders its own form and one that throws `Renderer is not defined` are the same object until
|
|
17
|
+
* the screen mounts. Declaring it is what lets the sign-in screen leave out a method whose
|
|
18
|
+
* renderer nobody registered, instead of offering a button that crashes the page it opens.
|
|
19
|
+
*/
|
|
20
|
+
requiresRenderer?: boolean
|
|
11
21
|
/**
|
|
12
22
|
* How this method presents itself on the sign-in screen.
|
|
13
23
|
*
|
|
@@ -39,6 +49,18 @@ export interface AuthMethodMeta {
|
|
|
39
49
|
restricted?: boolean
|
|
40
50
|
/** Registered, but never a choice. */
|
|
41
51
|
hidden?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Whether the application this method is about to be offered in actually wired it.
|
|
54
|
+
*
|
|
55
|
+
* The plugin registry is global and is written to by side-effect imports, so being registered
|
|
56
|
+
* says only that some bundle pulled the module in — and modules arrive in groups. An app that
|
|
57
|
+
* wanted one method out of a package gets every method that package registers, including ones
|
|
58
|
+
* whose first act is to look up a service its context never appended. This is where a plugin
|
|
59
|
+
* says so, per context, at render time; `false` leaves it off the screen.
|
|
60
|
+
*
|
|
61
|
+
* Absent means "always", which is right for a plugin that needs nothing but itself.
|
|
62
|
+
*/
|
|
63
|
+
available?: (ctx: LoginMethodContext) => boolean
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
export interface PluginImplemnetation {
|
package/tests/login.spec.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { makeTestContext } from './context.js'
|
|
|
3
3
|
import { appendLogin, makeLoginService } from '../src/login/service.js'
|
|
4
4
|
import { resolveLoginMethods, primaryLoginMethod, registerMethodSource } from '../src/login/methods.js'
|
|
5
5
|
import { resolveCredit } from '../src/login/credit.js'
|
|
6
|
-
import {
|
|
6
|
+
import { pluginMethodSource } from '../src/manager/plugins/methods.js'
|
|
7
|
+
import { registerAuthPlugin } from '../src/manager/plugins/registry.js'
|
|
8
|
+
import type { AuthenticationPlugin } from '../src/manager/plugins/types.js'
|
|
9
|
+
import { resumeAction, ResumeAction, loginAttemptError } from '../src/login/resume.js'
|
|
7
10
|
import { LoginOutcome } from '../src/login/types.js'
|
|
8
11
|
import type {
|
|
9
12
|
LoginEnv, LoginMethod, LoginMethodContext, LoginPlugin, LoginService,
|
|
@@ -170,12 +173,44 @@ describe('login method resolution', () => {
|
|
|
170
173
|
})
|
|
171
174
|
|
|
172
175
|
describe('the credit line', () => {
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
const year = new Date().getFullYear()
|
|
177
|
+
|
|
178
|
+
test('is a copyright NOTICE — the mark and the year, not a bare name', () => {
|
|
179
|
+
expect(resolveCredit({ product: 'OwlMeans', organization: 'OwlMeans' }).line)
|
|
180
|
+
.toBe(`© ${year} OwlMeans`)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('pairs the product with the notice when the product is not the holder', () => {
|
|
184
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme' }).line)
|
|
185
|
+
.toBe(`Taskly — © ${year} Acme`)
|
|
175
186
|
})
|
|
176
187
|
|
|
177
188
|
test('falls back to the entity slug when the organization has no name', () => {
|
|
178
|
-
expect(resolveCredit({ product: 'Taskly', entity: 'acme-7f2' }).line)
|
|
189
|
+
expect(resolveCredit({ product: 'Taskly', entity: 'acme-7f2' }).line)
|
|
190
|
+
.toBe(`Taskly — © ${year} acme-7f2`)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('a first year in the past makes the notice a range', () => {
|
|
194
|
+
expect(resolveCredit({ organization: 'Acme', since: 2019 }).line).toBe(`© 2019–${year} Acme`)
|
|
195
|
+
// The current year is not a range, and neither is one a skewed clock puts ahead of it.
|
|
196
|
+
expect(resolveCredit({ organization: 'Acme', since: year }).line).toBe(`© ${year} Acme`)
|
|
197
|
+
expect(resolveCredit({ organization: 'Acme', since: year + 5 }).line).toBe(`© ${year} Acme`)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('an explicit holder is who the notice names', () => {
|
|
201
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Taskly', holder: 'Acme Inc.' }).line)
|
|
202
|
+
.toBe(`Taskly — © ${year} Acme Inc.`)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('an owner\'s own wording is used verbatim', () => {
|
|
206
|
+
expect(resolveCredit({ organization: 'Acme', copyright: '© Acme. All rights reserved.' }).line)
|
|
207
|
+
.toBe('© Acme. All rights reserved.')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('switching the notice off leaves the bare pairing it used to be', () => {
|
|
211
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme', copyright: false }).line)
|
|
212
|
+
.toBe('Taskly — Acme')
|
|
213
|
+
expect(resolveCredit({ product: 'Taskly', copyright: false }).line).toBe('Taskly')
|
|
179
214
|
})
|
|
180
215
|
|
|
181
216
|
test('a literal line replaces the composition entirely', () => {
|
|
@@ -183,12 +218,88 @@ describe('the credit line', () => {
|
|
|
183
218
|
.toBe('© Acme')
|
|
184
219
|
})
|
|
185
220
|
|
|
221
|
+
test('nobody to name is nothing to render', () => {
|
|
222
|
+
expect(resolveCredit().line).toBeNull()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('an EMPTY value is unset, not a value', () => {
|
|
226
|
+
// Exactly what a platform delivers for branding it does not have: every key present, the
|
|
227
|
+
// undelivered ones empty. `??` does not fall through an empty string, so the organization
|
|
228
|
+
// used to win the chain and compose "pisteva — " with nothing after the dash.
|
|
229
|
+
expect(resolveCredit({}, { name: 'pisteva', organization: '', entity: '' }).line)
|
|
230
|
+
.toBe(`© ${year} pisteva`)
|
|
231
|
+
expect(resolveCredit({ line: '', product: '', organization: '' }, { name: 'Taskly' }).line)
|
|
232
|
+
.toBe(`© ${year} Taskly`)
|
|
233
|
+
expect(resolveCredit({ copyright: ' ' }, { name: 'Taskly' }).line).toBe(`© ${year} Taskly`)
|
|
234
|
+
expect(resolveCredit({}, {}, '').line).toBeNull()
|
|
235
|
+
})
|
|
236
|
+
|
|
186
237
|
test('the platform credit is on unless it is switched off', () => {
|
|
187
238
|
expect(resolveCredit().poweredBy).toBe(true)
|
|
188
239
|
expect(resolveCredit({ poweredBy: false }).poweredBy).toBe(false)
|
|
189
240
|
})
|
|
190
241
|
})
|
|
191
242
|
|
|
243
|
+
describe('the authentication-plugin method source', () => {
|
|
244
|
+
const plugin = (
|
|
245
|
+
over: Partial<AuthenticationPlugin> & { type: string }
|
|
246
|
+
): AuthenticationPlugin => ({ Implementation: () => () => null, ...over })
|
|
247
|
+
|
|
248
|
+
const offered = (context: BasicContext<BasicConfig>): string[] =>
|
|
249
|
+
pluginMethodSource.list(methodCtx(context)).map(item => item.id)
|
|
250
|
+
|
|
251
|
+
test('a plugin that declares no method is not a way to sign in', () => {
|
|
252
|
+
const context = makeTestContext()
|
|
253
|
+
registerAuthPlugin(plugin({ type: 'step-in-a-flow' }))
|
|
254
|
+
registerAuthPlugin(plugin({ type: 'offerable', method: {} }))
|
|
255
|
+
|
|
256
|
+
expect(offered(context)).toContain('offerable')
|
|
257
|
+
expect(offered(context)).not.toContain('step-in-a-flow')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('a plugin that needs a renderer nobody assigned is left off the screen', () => {
|
|
261
|
+
const context = makeTestContext()
|
|
262
|
+
// Exactly the state an app is in when it imported the plugin host but not a panel package:
|
|
263
|
+
// the plugin is registered, and mounting its screen throws.
|
|
264
|
+
registerAuthPlugin(plugin({ type: 'needs-ui', method: {}, requiresRenderer: true }))
|
|
265
|
+
|
|
266
|
+
expect(offered(context)).not.toContain('needs-ui')
|
|
267
|
+
|
|
268
|
+
registerAuthPlugin(plugin({
|
|
269
|
+
type: 'needs-ui', method: {}, requiresRenderer: true, Renderer: () => null,
|
|
270
|
+
}))
|
|
271
|
+
|
|
272
|
+
expect(offered(context)).toContain('needs-ui')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('a plugin whose own wiring is absent says so and is left off', () => {
|
|
276
|
+
const context = makeTestContext()
|
|
277
|
+
registerAuthPlugin(plugin({
|
|
278
|
+
type: 'needs-wiring', method: { available: ctx => ctx.context.hasService('never-appended') },
|
|
279
|
+
}))
|
|
280
|
+
|
|
281
|
+
expect(offered(context)).not.toContain('needs-wiring')
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('a finished sign-in attempt', () => {
|
|
286
|
+
test('reports every outcome that leaves the user on the screen', () => {
|
|
287
|
+
// The regression: a method that could not build an authorization URL returned `Passed`, the
|
|
288
|
+
// screen rendered nothing, and the button read as broken. `Passed` is a valid answer TO A
|
|
289
|
+
// DISPATCHER, which has a continuation; a screen does not.
|
|
290
|
+
expect(loginAttemptError(LoginOutcome.Passed)).toBe('login.error.failed')
|
|
291
|
+
expect(loginAttemptError(LoginOutcome.Failed)).toBe('login.error.failed')
|
|
292
|
+
expect(loginAttemptError(LoginOutcome.Gesture)).toBe('login.error.blocked')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('says nothing when the attempt actually went somewhere', () => {
|
|
296
|
+
expect(loginAttemptError(LoginOutcome.Handled)).toBeNull()
|
|
297
|
+
expect(loginAttemptError(LoginOutcome.Redirected)).toBeNull()
|
|
298
|
+
expect(loginAttemptError(LoginOutcome.Orphaned)).toBeNull()
|
|
299
|
+
expect(loginAttemptError(null)).toBeNull()
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
192
303
|
describe('resume outcomes', () => {
|
|
193
304
|
test('map to exactly one action each', () => {
|
|
194
305
|
expect(resumeAction(LoginOutcome.Handled)).toBe(ResumeAction.Stop)
|
package/build/.gitkeep
DELETED
|
File without changes
|