@owlmeans/client-auth 0.1.18-rc.12 → 0.1.18-rc.13
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 +1 -1
- package/build/login/adopt.d.ts +9 -0
- package/build/login/adopt.d.ts.map +1 -1
- package/build/login/adopt.js +11 -0
- package/build/login/adopt.js.map +1 -1
- package/build/login/consts.d.ts +25 -4
- package/build/login/consts.d.ts.map +1 -1
- package/build/login/consts.js +25 -4
- package/build/login/consts.js.map +1 -1
- package/build/login/credit.d.ts +19 -0
- package/build/login/credit.d.ts.map +1 -0
- package/build/login/credit.js +24 -0
- package/build/login/credit.js.map +1 -0
- package/build/login/hook.d.ts +14 -3
- package/build/login/hook.d.ts.map +1 -1
- package/build/login/hook.js +27 -9
- package/build/login/hook.js.map +1 -1
- package/build/login/i18n/be.json +37 -0
- package/build/login/i18n/de.json +37 -0
- package/build/login/i18n/en.json +37 -0
- package/build/login/i18n/es.json +37 -0
- package/build/login/i18n/pl.json +37 -0
- package/build/login/i18n/ru.json +37 -0
- package/build/login/i18n/uk.json +37 -0
- package/build/login/i18n.d.ts +2 -0
- package/build/login/i18n.d.ts.map +1 -0
- package/build/login/i18n.js +25 -0
- package/build/login/i18n.js.map +1 -0
- package/build/login/index.d.ts +7 -0
- package/build/login/index.d.ts.map +1 -1
- package/build/login/index.js +7 -0
- package/build/login/index.js.map +1 -1
- package/build/login/methods.d.ts +25 -0
- package/build/login/methods.d.ts.map +1 -0
- package/build/login/methods.js +97 -0
- package/build/login/methods.js.map +1 -0
- package/build/login/resume.d.ts +20 -0
- package/build/login/resume.d.ts.map +1 -0
- package/build/login/resume.js +33 -0
- package/build/login/resume.js.map +1 -0
- package/build/login/screen.d.ts +4 -0
- package/build/login/screen.d.ts.map +1 -0
- package/build/login/screen.js +63 -0
- package/build/login/screen.js.map +1 -0
- package/build/login/service.d.ts +4 -3
- package/build/login/service.d.ts.map +1 -1
- package/build/login/service.js +57 -4
- package/build/login/service.js.map +1 -1
- package/build/login/surrogate.d.ts +20 -0
- package/build/login/surrogate.d.ts.map +1 -0
- package/build/login/surrogate.js +33 -0
- package/build/login/surrogate.js.map +1 -0
- package/build/login/terms.d.ts +20 -0
- package/build/login/terms.d.ts.map +1 -0
- package/build/login/terms.js +0 -0
- package/build/login/terms.js.map +1 -0
- package/build/login/types.d.ts +123 -1
- package/build/login/types.d.ts.map +1 -1
- package/build/login/types.js +7 -1
- package/build/login/types.js.map +1 -1
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.js +1 -0
- package/build/manager/plugins/basic-ed25519.js.map +1 -1
- package/build/manager/plugins/exports.d.ts +2 -0
- package/build/manager/plugins/exports.d.ts.map +1 -1
- package/build/manager/plugins/exports.js +2 -0
- package/build/manager/plugins/exports.js.map +1 -1
- package/build/manager/plugins/index.d.ts +1 -5
- package/build/manager/plugins/index.d.ts.map +1 -1
- package/build/manager/plugins/index.js +9 -1
- package/build/manager/plugins/index.js.map +1 -1
- package/build/manager/plugins/methods.d.ts +10 -0
- package/build/manager/plugins/methods.d.ts.map +1 -0
- package/build/manager/plugins/methods.js +46 -0
- package/build/manager/plugins/methods.js.map +1 -0
- package/build/manager/plugins/re-captcha.d.ts.map +1 -1
- package/build/manager/plugins/re-captcha.js +2 -0
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/registry.d.ts +20 -0
- package/build/manager/plugins/registry.d.ts.map +1 -0
- package/build/manager/plugins/registry.js +19 -0
- package/build/manager/plugins/registry.js.map +1 -0
- package/build/manager/plugins/tunnel-consumer.d.ts.map +1 -1
- package/build/manager/plugins/tunnel-consumer.js +1 -0
- package/build/manager/plugins/tunnel-consumer.js.map +1 -1
- package/build/manager/plugins/types.d.ts +31 -0
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/package.json +20 -18
- package/src/login/adopt.ts +12 -0
- package/src/login/consts.ts +31 -4
- package/src/login/credit.ts +37 -0
- package/src/login/hook.ts +28 -11
- package/src/login/i18n/be.json +37 -0
- package/src/login/i18n/de.json +37 -0
- package/src/login/i18n/en.json +37 -0
- package/src/login/i18n/es.json +37 -0
- package/src/login/i18n/pl.json +37 -0
- package/src/login/i18n/ru.json +37 -0
- package/src/login/i18n/uk.json +37 -0
- package/src/login/i18n.ts +26 -0
- package/src/login/index.ts +8 -0
- package/src/login/methods.ts +113 -0
- package/src/login/resume.ts +33 -0
- package/src/login/screen.tsx +122 -0
- package/src/login/service.ts +78 -5
- package/src/login/surrogate.ts +45 -0
- package/src/login/terms.ts +0 -0
- package/src/login/types.ts +132 -1
- package/src/manager/plugins/basic-ed25519.tsx +2 -0
- package/src/manager/plugins/exports.ts +2 -0
- package/src/manager/plugins/index.ts +10 -4
- package/src/manager/plugins/methods.ts +52 -0
- package/src/manager/plugins/re-captcha.tsx +3 -0
- package/src/manager/plugins/registry.ts +26 -0
- package/src/manager/plugins/tunnel-consumer.tsx +2 -0
- package/src/manager/plugins/types.ts +32 -0
- package/tests/login.spec.ts +202 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { makeTestContext } from './context.js'
|
|
3
|
+
import { appendLogin, makeLoginService } from '../src/login/service.js'
|
|
4
|
+
import { resolveLoginMethods, primaryLoginMethod, registerMethodSource } from '../src/login/methods.js'
|
|
5
|
+
import { resolveCredit } from '../src/login/credit.js'
|
|
6
|
+
import { resumeAction, ResumeAction } from '../src/login/resume.js'
|
|
7
|
+
import { LoginOutcome } from '../src/login/types.js'
|
|
8
|
+
import type {
|
|
9
|
+
LoginEnv, LoginMethod, LoginMethodContext, LoginPlugin, LoginService,
|
|
10
|
+
} from '../src/login/types.js'
|
|
11
|
+
import type { BasicContext, BasicConfig } from '@owlmeans/context'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A hand-written environment descriptor.
|
|
15
|
+
*
|
|
16
|
+
* `LoginEnv` exists precisely so a plugin's decisions can be exercised without a DOM — a stubbed
|
|
17
|
+
* `window` would prove that the stub behaves, not that the cascade does.
|
|
18
|
+
*/
|
|
19
|
+
const env = (over?: Partial<LoginEnv>): LoginEnv => ({
|
|
20
|
+
hasWindow: true, embedded: false, surrogate: false, hasOpener: false, ...over,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const stubPlugin = (over: Partial<LoginPlugin> & { alias: string }): LoginPlugin => ({
|
|
24
|
+
begin: async () => LoginOutcome.Passed,
|
|
25
|
+
authorize: async () => LoginOutcome.Passed,
|
|
26
|
+
complete: async () => LoginOutcome.Passed,
|
|
27
|
+
...over,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const bootstrap = (): [BasicContext<BasicConfig>, LoginService] => {
|
|
31
|
+
const context = makeTestContext()
|
|
32
|
+
const contextual = appendLogin(context)
|
|
33
|
+
|
|
34
|
+
return [context, contextual.login()]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const methodCtx = (context: BasicContext<BasicConfig>, over?: Partial<LoginEnv>): LoginMethodContext =>
|
|
38
|
+
({ context, env: env(over) })
|
|
39
|
+
|
|
40
|
+
const method = (over: Partial<LoginMethod> & { id: string }): LoginMethod => ({
|
|
41
|
+
start: async () => LoginOutcome.Passed, ...over,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('login plugin host', () => {
|
|
45
|
+
test('the cascade picks the highest-priority plugin whose match holds', () => {
|
|
46
|
+
const service = makeLoginService()
|
|
47
|
+
service.registerPlugin(stubPlugin({ alias: 'default', priority: 0, match: () => true }))
|
|
48
|
+
service.registerPlugin(stubPlugin({
|
|
49
|
+
alias: 'framed', priority: 100, match: candidate => candidate.embedded,
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
expect(service.plugin(env()).alias).toBe('default')
|
|
53
|
+
expect(service.plugin(env({ embedded: true })).alias).toBe('framed')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a plugin with no resume yields Passed, so an ordinary tab is untouched', async () => {
|
|
57
|
+
const [, service] = bootstrap()
|
|
58
|
+
service.registerPlugin(stubPlugin({ alias: 'plain', match: () => true }))
|
|
59
|
+
|
|
60
|
+
expect(await service.resume('a-token')).toBe(LoginOutcome.Passed)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a plugin with a resume is asked instead', async () => {
|
|
64
|
+
const [, service] = bootstrap()
|
|
65
|
+
service.registerPlugin(stubPlugin({
|
|
66
|
+
alias: 'framed', match: () => true, resume: async () => LoginOutcome.Handled,
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
expect(await service.resume('a-token')).toBe(LoginOutcome.Handled)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('a refusing precondition stops the flow before any plugin runs', async () => {
|
|
73
|
+
const [, service] = bootstrap()
|
|
74
|
+
let started = false
|
|
75
|
+
service.registerPlugin(stubPlugin({
|
|
76
|
+
alias: 'plain', match: () => true,
|
|
77
|
+
begin: async () => { started = true; return LoginOutcome.Handled },
|
|
78
|
+
}))
|
|
79
|
+
service.registerPrecondition({ alias: 'refuse', check: () => false })
|
|
80
|
+
|
|
81
|
+
expect(await service.begin({ url: '/dispatcher' })).toBe(LoginOutcome.Gesture)
|
|
82
|
+
expect(started).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('a passing precondition lets the plugin run', async () => {
|
|
86
|
+
const [, service] = bootstrap()
|
|
87
|
+
service.registerPlugin(stubPlugin({
|
|
88
|
+
alias: 'plain', match: () => true, begin: async () => LoginOutcome.Handled,
|
|
89
|
+
}))
|
|
90
|
+
service.registerPrecondition({ alias: 'allow', check: () => true })
|
|
91
|
+
|
|
92
|
+
expect(await service.begin({ url: '/dispatcher' })).toBe(LoginOutcome.Handled)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('a plugin with no logout still ends the session it was asked to end', async () => {
|
|
96
|
+
const [context, service] = bootstrap()
|
|
97
|
+
context.configure()
|
|
98
|
+
await context.init()
|
|
99
|
+
service.registerPlugin(stubPlugin({ alias: 'plain', match: () => true }))
|
|
100
|
+
const auth = context.service<{ token?: string, update: (t?: string) => Promise<void> }>('auth')
|
|
101
|
+
|
|
102
|
+
expect(await service.logout({ url: '' })).toBe(LoginOutcome.Passed)
|
|
103
|
+
expect(auth.token == null || auth.token === '').toBe(true)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('login method resolution', () => {
|
|
108
|
+
test('methods sort by declared order, ties by id', () => {
|
|
109
|
+
const context = makeTestContext()
|
|
110
|
+
const source = {
|
|
111
|
+
alias: 'test', list: () => [
|
|
112
|
+
method({ id: 'b', order: 10 }), method({ id: 'a', order: 10 }), method({ id: 'z', order: 1 }),
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resolved = resolveLoginMethods(methodCtx(context), undefined, [source])
|
|
117
|
+
|
|
118
|
+
expect(resolved.map(item => item.id)).toEqual(['z', 'a', 'b'])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('a restricted method is not offered until the configuration asks for it', () => {
|
|
122
|
+
const context = makeTestContext()
|
|
123
|
+
const source = {
|
|
124
|
+
alias: 'test', list: () => [method({ id: 'ordinary' }), method({ id: 'operator', restricted: true })],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
expect(resolveLoginMethods(methodCtx(context), undefined, [source]).map(m => m.id))
|
|
128
|
+
.toEqual(['ordinary'])
|
|
129
|
+
expect(resolveLoginMethods(
|
|
130
|
+
methodCtx(context), { overrides: { operator: { enabled: true } } }, [source]
|
|
131
|
+
).map(m => m.id).sort()).toEqual(['operator', 'ordinary'])
|
|
132
|
+
expect(resolveLoginMethods(
|
|
133
|
+
methodCtx(context), { methods: ['operator'] }, [source]
|
|
134
|
+
).map(m => m.id)).toEqual(['operator'])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('an explicit list is both the filter and the order', () => {
|
|
138
|
+
const context = makeTestContext()
|
|
139
|
+
const source = {
|
|
140
|
+
alias: 'test', list: () => [method({ id: 'a' }), method({ id: 'b' }), method({ id: 'c' })],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
expect(resolveLoginMethods(methodCtx(context), { methods: ['c', 'a'] }, [source]).map(m => m.id))
|
|
144
|
+
.toEqual(['c', 'a'])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('a source that throws is skipped rather than taking the screen down', () => {
|
|
148
|
+
const context = makeTestContext()
|
|
149
|
+
const broken = { alias: 'broken', list: () => { throw new Error('misconfigured') } }
|
|
150
|
+
const working = { alias: 'working', list: () => [method({ id: 'ok' })] }
|
|
151
|
+
|
|
152
|
+
expect(resolveLoginMethods(methodCtx(context), undefined, [broken, working]).map(m => m.id))
|
|
153
|
+
.toEqual(['ok'])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('the primary method is the first emphasised one, else the first', () => {
|
|
157
|
+
expect(primaryLoginMethod([
|
|
158
|
+
method({ id: 'a' }), method({ id: 'b', emphasis: 'primary' }),
|
|
159
|
+
])?.id).toBe('b')
|
|
160
|
+
expect(primaryLoginMethod([method({ id: 'a' })])?.id).toBe('a')
|
|
161
|
+
expect(primaryLoginMethod([])).toBeNull()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('a globally registered source reaches the resolver', () => {
|
|
165
|
+
const context = makeTestContext()
|
|
166
|
+
registerMethodSource({ alias: 'global-test', list: () => [method({ id: 'global' })] })
|
|
167
|
+
|
|
168
|
+
expect(resolveLoginMethods(methodCtx(context)).map(m => m.id)).toContain('global')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('the credit line', () => {
|
|
173
|
+
test('pairs the product with the organization', () => {
|
|
174
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme' }).line).toBe('Taskly — Acme')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('falls back to the entity slug when the organization has no name', () => {
|
|
178
|
+
expect(resolveCredit({ product: 'Taskly', entity: 'acme-7f2' }).line).toBe('Taskly — acme-7f2')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('a literal line replaces the composition entirely', () => {
|
|
182
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme', line: '© Acme' }).line)
|
|
183
|
+
.toBe('© Acme')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('the platform credit is on unless it is switched off', () => {
|
|
187
|
+
expect(resolveCredit().poweredBy).toBe(true)
|
|
188
|
+
expect(resolveCredit({ poweredBy: false }).poweredBy).toBe(false)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('resume outcomes', () => {
|
|
193
|
+
test('map to exactly one action each', () => {
|
|
194
|
+
expect(resumeAction(LoginOutcome.Handled)).toBe(ResumeAction.Stop)
|
|
195
|
+
expect(resumeAction(LoginOutcome.Redirected)).toBe(ResumeAction.Stop)
|
|
196
|
+
expect(resumeAction(LoginOutcome.Orphaned)).toBe(ResumeAction.Render)
|
|
197
|
+
expect(resumeAction(LoginOutcome.Failed)).toBe(ResumeAction.Render)
|
|
198
|
+
expect(resumeAction(LoginOutcome.Gesture)).toBe(ResumeAction.Render)
|
|
199
|
+
// The ordinary tab: keep the session and carry on. This is the one that must never change.
|
|
200
|
+
expect(resumeAction(LoginOutcome.Passed)).toBe(ResumeAction.Navigate)
|
|
201
|
+
})
|
|
202
|
+
})
|