@atproto/bsky 0.0.198 → 0.0.199
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/CHANGELOG.md +14 -0
- package/dist/api/age-assurance/const.d.ts +11 -0
- package/dist/api/age-assurance/const.d.ts.map +1 -0
- package/dist/api/age-assurance/const.js +142 -0
- package/dist/api/age-assurance/const.js.map +1 -0
- package/dist/api/age-assurance/index.d.ts +4 -0
- package/dist/api/age-assurance/index.d.ts.map +1 -0
- package/dist/api/age-assurance/index.js +24 -0
- package/dist/api/age-assurance/index.js.map +1 -0
- package/dist/api/age-assurance/kws/age-verified.d.ts +109 -0
- package/dist/api/age-assurance/kws/age-verified.d.ts.map +1 -0
- package/dist/api/age-assurance/kws/age-verified.js +63 -0
- package/dist/api/age-assurance/kws/age-verified.js.map +1 -0
- package/dist/api/age-assurance/kws/const.d.ts +13 -0
- package/dist/api/age-assurance/kws/const.d.ts.map +1 -0
- package/dist/api/age-assurance/kws/const.js +36 -0
- package/dist/api/age-assurance/kws/const.js.map +1 -0
- package/dist/api/age-assurance/kws/external-payload.d.ts +75 -0
- package/dist/api/age-assurance/kws/external-payload.d.ts.map +1 -0
- package/dist/api/age-assurance/kws/external-payload.js +124 -0
- package/dist/api/age-assurance/kws/external-payload.js.map +1 -0
- package/dist/api/age-assurance/kws/external-payload.test.d.ts +2 -0
- package/dist/api/age-assurance/kws/external-payload.test.d.ts.map +1 -0
- package/dist/api/age-assurance/kws/external-payload.test.js +65 -0
- package/dist/api/age-assurance/kws/external-payload.test.js.map +1 -0
- package/dist/api/age-assurance/redirects/kws-age-verified.d.ts +4 -0
- package/dist/api/age-assurance/redirects/kws-age-verified.d.ts.map +1 -0
- package/dist/api/age-assurance/redirects/kws-age-verified.js +76 -0
- package/dist/api/age-assurance/redirects/kws-age-verified.js.map +1 -0
- package/dist/api/age-assurance/stash.d.ts +4 -0
- package/dist/api/age-assurance/stash.d.ts.map +1 -0
- package/dist/api/age-assurance/stash.js +19 -0
- package/dist/api/age-assurance/stash.js.map +1 -0
- package/dist/api/age-assurance/types.d.ts +10 -0
- package/dist/api/age-assurance/types.d.ts.map +1 -0
- package/dist/api/age-assurance/types.js +3 -0
- package/dist/api/age-assurance/types.js.map +1 -0
- package/dist/api/age-assurance/util.d.ts +15 -0
- package/dist/api/age-assurance/util.d.ts.map +1 -0
- package/dist/api/age-assurance/util.js +54 -0
- package/dist/api/age-assurance/util.js.map +1 -0
- package/dist/api/age-assurance/webhooks/kws-age-verified.d.ts +4 -0
- package/dist/api/age-assurance/webhooks/kws-age-verified.d.ts.map +1 -0
- package/dist/api/age-assurance/webhooks/kws-age-verified.js +63 -0
- package/dist/api/age-assurance/webhooks/kws-age-verified.js.map +1 -0
- package/dist/api/app/bsky/ageassurance/begin.d.ts +4 -0
- package/dist/api/app/bsky/ageassurance/begin.d.ts.map +1 -0
- package/dist/api/app/bsky/ageassurance/begin.js +131 -0
- package/dist/api/app/bsky/ageassurance/begin.js.map +1 -0
- package/dist/api/app/bsky/ageassurance/getConfig.d.ts +4 -0
- package/dist/api/app/bsky/ageassurance/getConfig.d.ts.map +1 -0
- package/dist/api/app/bsky/ageassurance/getConfig.js +16 -0
- package/dist/api/app/bsky/ageassurance/getConfig.js.map +1 -0
- package/dist/api/app/bsky/ageassurance/getState.d.ts +4 -0
- package/dist/api/app/bsky/ageassurance/getState.d.ts.map +1 -0
- package/dist/api/app/bsky/ageassurance/getState.js +42 -0
- package/dist/api/app/bsky/ageassurance/getState.js.map +1 -0
- package/dist/api/external.d.ts.map +1 -1
- package/dist/api/external.js +2 -0
- package/dist/api/external.js.map +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +8 -2
- package/dist/api/index.js.map +1 -1
- package/dist/api/kws/api.d.ts.map +1 -1
- package/dist/api/kws/api.js +44 -26
- package/dist/api/kws/api.js.map +1 -1
- package/dist/api/kws/index.d.ts.map +1 -1
- package/dist/api/kws/index.js +3 -1
- package/dist/api/kws/index.js.map +1 -1
- package/dist/api/kws/webhook.d.ts +3 -1
- package/dist/api/kws/webhook.d.ts.map +1 -1
- package/dist/api/kws/webhook.js +48 -20
- package/dist/api/kws/webhook.js.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -2
- package/dist/config.js.map +1 -1
- package/dist/data-plane/bsync/index.d.ts.map +1 -1
- package/dist/data-plane/bsync/index.js +22 -0
- package/dist/data-plane/bsync/index.js.map +1 -1
- package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.d.ts +4 -0
- package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.d.ts.map +1 -0
- package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.js +30 -0
- package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.js.map +1 -0
- package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
- package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
- package/dist/data-plane/server/db/migrations/index.js +2 -1
- package/dist/data-plane/server/db/migrations/index.js.map +1 -1
- package/dist/data-plane/server/db/pagination.d.ts +3 -3
- package/dist/data-plane/server/db/tables/actor.d.ts +3 -0
- package/dist/data-plane/server/db/tables/actor.d.ts.map +1 -1
- package/dist/data-plane/server/db/tables/actor.js.map +1 -1
- package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
- package/dist/data-plane/server/routes/profile.js +13 -1
- package/dist/data-plane/server/routes/profile.js.map +1 -1
- package/dist/hydration/hydrator.js +1 -1
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/kws.d.ts +35 -0
- package/dist/kws.d.ts.map +1 -1
- package/dist/kws.js +54 -0
- package/dist/kws.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +2 -1
- package/dist/logger.js.map +1 -1
- package/dist/proto/bsky_pb.d.ts +8 -0
- package/dist/proto/bsky_pb.d.ts.map +1 -1
- package/dist/proto/bsky_pb.js +20 -0
- package/dist/proto/bsky_pb.js.map +1 -1
- package/dist/stash.d.ts +1 -0
- package/dist/stash.d.ts.map +1 -1
- package/dist/stash.js +1 -0
- package/dist/stash.js.map +1 -1
- package/dist/util/uris.d.ts +2 -2
- package/dist/util/uris.d.ts.map +1 -1
- package/package.json +10 -9
- package/proto/bsky.proto +1 -0
- package/src/api/age-assurance/const.ts +142 -0
- package/src/api/age-assurance/index.ts +34 -0
- package/src/api/age-assurance/kws/age-verified.ts +75 -0
- package/src/api/age-assurance/kws/const.ts +33 -0
- package/src/api/age-assurance/kws/external-payload.test.ts +72 -0
- package/src/api/age-assurance/kws/external-payload.ts +149 -0
- package/src/api/age-assurance/redirects/kws-age-verified.ts +107 -0
- package/src/api/age-assurance/stash.ts +22 -0
- package/src/api/age-assurance/types.ts +10 -0
- package/src/api/age-assurance/util.ts +66 -0
- package/src/api/age-assurance/webhooks/kws-age-verified.ts +75 -0
- package/src/api/app/bsky/ageassurance/begin.ts +167 -0
- package/src/api/app/bsky/ageassurance/getConfig.ts +15 -0
- package/src/api/app/bsky/ageassurance/getState.ts +53 -0
- package/src/api/external.ts +2 -0
- package/src/api/index.ts +6 -0
- package/src/api/kws/api.ts +55 -34
- package/src/api/kws/index.ts +7 -1
- package/src/api/kws/webhook.ts +57 -34
- package/src/config.ts +26 -2
- package/src/data-plane/bsync/index.ts +31 -0
- package/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts +28 -0
- package/src/data-plane/server/db/migrations/index.ts +1 -0
- package/src/data-plane/server/db/tables/actor.ts +3 -0
- package/src/data-plane/server/routes/profile.ts +12 -1
- package/src/hydration/hydrator.ts +1 -1
- package/src/kws.ts +81 -0
- package/src/logger.ts +2 -0
- package/src/proto/bsky_pb.ts +12 -0
- package/src/stash.ts +3 -0
- package/tests/views/age-assurance-v2.test.ts +745 -0
- package/tests/views/age-assurance.test.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import express, { RequestHandler } from 'express'
|
|
2
|
+
import { ageAssuranceLogger as logger } from '../../../logger'
|
|
3
|
+
import { getClientUa, validateSignature } from '../../kws/util'
|
|
4
|
+
import { AGE_ASSURANCE_CONFIG } from '../const'
|
|
5
|
+
import { parseKWSAgeVerifiedStatus } from '../kws/age-verified'
|
|
6
|
+
import {
|
|
7
|
+
type KWSExternalPayloadV2,
|
|
8
|
+
parseKWSExternalPayloadV2,
|
|
9
|
+
} from '../kws/external-payload'
|
|
10
|
+
import { createEvent } from '../stash'
|
|
11
|
+
import { AppContextWithAA } from '../types'
|
|
12
|
+
import { computeAgeAssuranceAccessOrThrow } from '../util'
|
|
13
|
+
|
|
14
|
+
function parseQueryParams(
|
|
15
|
+
ctx: AppContextWithAA,
|
|
16
|
+
req: express.Request,
|
|
17
|
+
): {
|
|
18
|
+
status: string
|
|
19
|
+
externalPayload: string
|
|
20
|
+
} {
|
|
21
|
+
try {
|
|
22
|
+
const status = String(req.query.status)
|
|
23
|
+
const externalPayload = String(req.query.externalPayload)
|
|
24
|
+
const signature = String(req.query.signature)
|
|
25
|
+
|
|
26
|
+
validateSignature(
|
|
27
|
+
ctx.cfg.kws.ageVerifiedRedirectSecret,
|
|
28
|
+
`${status}:${externalPayload}`,
|
|
29
|
+
signature,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
status,
|
|
34
|
+
externalPayload,
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error('Invalid KWS API request', { cause: err })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const handler =
|
|
42
|
+
(ctx: AppContextWithAA): RequestHandler =>
|
|
43
|
+
async (req: express.Request, res: express.Response) => {
|
|
44
|
+
let externalPayload: KWSExternalPayloadV2 | undefined
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const query = parseQueryParams(ctx, req)
|
|
48
|
+
const { verified, verifiedMinimumAge } = parseKWSAgeVerifiedStatus(
|
|
49
|
+
query.status,
|
|
50
|
+
)
|
|
51
|
+
externalPayload = parseKWSExternalPayloadV2(query.externalPayload)
|
|
52
|
+
const { actorDid, attemptId, countryCode, regionCode } = externalPayload
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* KWS does not send unverified webhooks for age verification, so we
|
|
56
|
+
* expect all webhooks to be verified. This is just a sanity check.
|
|
57
|
+
*/
|
|
58
|
+
if (!verified) {
|
|
59
|
+
const message =
|
|
60
|
+
'Expected KWS verification redirect to have verified status'
|
|
61
|
+
logger.error({}, message)
|
|
62
|
+
throw new Error(message)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { access } = computeAgeAssuranceAccessOrThrow(
|
|
66
|
+
AGE_ASSURANCE_CONFIG,
|
|
67
|
+
{
|
|
68
|
+
countryCode,
|
|
69
|
+
regionCode,
|
|
70
|
+
verifiedMinimumAge,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
await createEvent(ctx, actorDid, {
|
|
75
|
+
attemptId,
|
|
76
|
+
// Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
|
|
77
|
+
completeIp: req.ip,
|
|
78
|
+
completeUa: getClientUa(req),
|
|
79
|
+
countryCode,
|
|
80
|
+
regionCode,
|
|
81
|
+
status: 'assured',
|
|
82
|
+
access,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const q = new URLSearchParams({ actorDid, result: 'success' })
|
|
86
|
+
|
|
87
|
+
return res
|
|
88
|
+
.status(302)
|
|
89
|
+
.setHeader('Location', `${ctx.cfg.kws.redirectUrl}?${q}`)
|
|
90
|
+
.end()
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger.error(
|
|
93
|
+
{ err, ...externalPayload },
|
|
94
|
+
'Failed to handle KWS verification redirect',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const q = new URLSearchParams({
|
|
98
|
+
...(externalPayload ? { actorDid: externalPayload.actorDid } : {}),
|
|
99
|
+
result: 'unknown',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return res
|
|
103
|
+
.status(302)
|
|
104
|
+
.setHeader('Location', `${ctx.cfg.kws.redirectUrl}?${q}`)
|
|
105
|
+
.end()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TID } from '@atproto/common'
|
|
2
|
+
import { AppContext } from '../../context'
|
|
3
|
+
import { Event as AgeAssuranceEvent } from '../../lexicon/types/app/bsky/ageassurance/defs'
|
|
4
|
+
import { Namespaces } from '../../stash'
|
|
5
|
+
|
|
6
|
+
export async function createEvent(
|
|
7
|
+
ctx: AppContext,
|
|
8
|
+
actorDid: string,
|
|
9
|
+
event: Omit<AgeAssuranceEvent, 'createdAt'>,
|
|
10
|
+
) {
|
|
11
|
+
const payload: AgeAssuranceEvent = {
|
|
12
|
+
createdAt: new Date().toISOString(),
|
|
13
|
+
...event,
|
|
14
|
+
}
|
|
15
|
+
await ctx.stashClient.create({
|
|
16
|
+
actorDid: actorDid,
|
|
17
|
+
namespace: Namespaces.AppBskyAgeassuranceDefsEvent,
|
|
18
|
+
key: TID.nextStr(),
|
|
19
|
+
payload,
|
|
20
|
+
})
|
|
21
|
+
return payload
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KwsConfig, ServerConfig } from '../../config'
|
|
2
|
+
import { AppContext } from '../../context'
|
|
3
|
+
import { KwsClient } from '../../kws'
|
|
4
|
+
|
|
5
|
+
export type AppContextWithAA = AppContext & {
|
|
6
|
+
kwsClient: KwsClient
|
|
7
|
+
cfg: ServerConfig & {
|
|
8
|
+
kws: KwsConfig
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AppBskyAgeassuranceDefs,
|
|
3
|
+
computeAgeAssuranceRegionAccess,
|
|
4
|
+
getAgeAssuranceRegionConfig,
|
|
5
|
+
} from '@atproto/api'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compute age assurance access based on verified minimum age. Thrown errors
|
|
9
|
+
* are internal errors, so handle them accordingly.
|
|
10
|
+
*/
|
|
11
|
+
export function computeAgeAssuranceAccessOrThrow(
|
|
12
|
+
config: AppBskyAgeassuranceDefs.Config,
|
|
13
|
+
{
|
|
14
|
+
countryCode,
|
|
15
|
+
regionCode,
|
|
16
|
+
verifiedMinimumAge,
|
|
17
|
+
}: {
|
|
18
|
+
countryCode: string
|
|
19
|
+
regionCode?: string
|
|
20
|
+
verifiedMinimumAge: number
|
|
21
|
+
},
|
|
22
|
+
) {
|
|
23
|
+
const region = getAgeAssuranceRegionConfig(config, {
|
|
24
|
+
countryCode,
|
|
25
|
+
regionCode,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (region) {
|
|
29
|
+
const result = computeAgeAssuranceRegionAccess(region, {
|
|
30
|
+
assuredAge: verifiedMinimumAge,
|
|
31
|
+
/*
|
|
32
|
+
* We don't care about this here, this is a client-only rule. If we have
|
|
33
|
+
* verified data, we can use that, and the account creation date is
|
|
34
|
+
* irrelevant.
|
|
35
|
+
*/
|
|
36
|
+
accountCreatedAt: undefined,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
if (result) {
|
|
40
|
+
return result
|
|
41
|
+
} else {
|
|
42
|
+
/*
|
|
43
|
+
* If we don't get a result, it's because none of the rules matched,
|
|
44
|
+
* which is a configuration error: there should always be a default
|
|
45
|
+
* rule.
|
|
46
|
+
*/
|
|
47
|
+
throw new Error('Cound not compute age assurance region access')
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
/**
|
|
51
|
+
* If we had geolocation data, but we don't have a region config for this
|
|
52
|
+
* geolocation, then it means a user outside of our configured regions
|
|
53
|
+
* has completed age verification. In this case, we can't determine their
|
|
54
|
+
* access level, so we throw an error.
|
|
55
|
+
*
|
|
56
|
+
* This case is also guarded in `app.bsky.ageassurance.begin`.
|
|
57
|
+
*/
|
|
58
|
+
throw new Error('Could not get config for region')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createLocationString(countryCode: string, regionCode?: string) {
|
|
63
|
+
return regionCode
|
|
64
|
+
? `${countryCode.toUpperCase()}-${regionCode.toUpperCase()}`
|
|
65
|
+
: countryCode.toUpperCase()
|
|
66
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import express, { RequestHandler } from 'express'
|
|
2
|
+
import { ageAssuranceLogger as logger } from '../../../logger'
|
|
3
|
+
import { AGE_ASSURANCE_CONFIG } from '../const'
|
|
4
|
+
import {
|
|
5
|
+
type KWSWebhookAgeVerified,
|
|
6
|
+
parseKWSAgeVerifiedWebhook,
|
|
7
|
+
} from '../kws/age-verified'
|
|
8
|
+
import { parseKWSExternalPayloadV2 } from '../kws/external-payload'
|
|
9
|
+
import { createEvent } from '../stash'
|
|
10
|
+
import { type AppContextWithAA } from '../types'
|
|
11
|
+
import { computeAgeAssuranceAccessOrThrow } from '../util'
|
|
12
|
+
|
|
13
|
+
export const handler =
|
|
14
|
+
(ctx: AppContextWithAA): RequestHandler =>
|
|
15
|
+
async (req: express.Request, res: express.Response) => {
|
|
16
|
+
let body: KWSWebhookAgeVerified
|
|
17
|
+
try {
|
|
18
|
+
body = parseKWSAgeVerifiedWebhook(req.body)
|
|
19
|
+
} catch (err) {
|
|
20
|
+
const message = 'Failed to parse KWS webhook body'
|
|
21
|
+
logger.error({ err }, message)
|
|
22
|
+
return res.status(400).json({ error: message })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { status, externalPayload } = body.payload
|
|
26
|
+
const { verified, verifiedMinimumAge } = status
|
|
27
|
+
const { actorDid, countryCode, regionCode, attemptId } =
|
|
28
|
+
parseKWSExternalPayloadV2(externalPayload)
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* KWS does not send unverified webhooks for age verification, so we
|
|
32
|
+
* expect all webhooks to be verified. This is just a sanity check.
|
|
33
|
+
*/
|
|
34
|
+
if (!verified) {
|
|
35
|
+
const message = 'Expected KWS webhook to have verified status'
|
|
36
|
+
logger.error({}, message)
|
|
37
|
+
return res.status(400).json({ error: message })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let result: ReturnType<typeof computeAgeAssuranceAccessOrThrow> | undefined
|
|
41
|
+
try {
|
|
42
|
+
result = computeAgeAssuranceAccessOrThrow(AGE_ASSURANCE_CONFIG, {
|
|
43
|
+
countryCode,
|
|
44
|
+
regionCode,
|
|
45
|
+
verifiedMinimumAge,
|
|
46
|
+
})
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// internal errors
|
|
49
|
+
logger.error(
|
|
50
|
+
{ err, attemptId, actorDid, countryCode, regionCode },
|
|
51
|
+
'Failed to compute age assurance access',
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (result) {
|
|
57
|
+
await createEvent(ctx, actorDid, {
|
|
58
|
+
attemptId,
|
|
59
|
+
countryCode,
|
|
60
|
+
regionCode,
|
|
61
|
+
status: 'assured',
|
|
62
|
+
access: result.access,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return res.status(200).end()
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const message = 'Failed to handle KWS webhook'
|
|
69
|
+
logger.error(
|
|
70
|
+
{ err, attemptId, actorDid, countryCode, regionCode },
|
|
71
|
+
message,
|
|
72
|
+
)
|
|
73
|
+
return res.status(500).json({ error: message })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { isEmailValid } from '@hapi/address'
|
|
3
|
+
import { isDisposableEmail } from 'disposable-email-domains-js'
|
|
4
|
+
import { getAgeAssuranceRegionConfig } from '@atproto/api'
|
|
5
|
+
import {
|
|
6
|
+
InvalidRequestError,
|
|
7
|
+
MethodNotImplementedError,
|
|
8
|
+
} from '@atproto/xrpc-server'
|
|
9
|
+
import { AppContext } from '../../../../context'
|
|
10
|
+
import { Server } from '../../../../lexicon'
|
|
11
|
+
import { InputSchema } from '../../../../lexicon/types/app/bsky/ageassurance/begin'
|
|
12
|
+
import { httpLogger as log } from '../../../../logger'
|
|
13
|
+
import { ActorInfo } from '../../../../proto/bsky_pb'
|
|
14
|
+
import { AGE_ASSURANCE_CONFIG } from '../../../age-assurance/const'
|
|
15
|
+
import {
|
|
16
|
+
KWS_SUPPORTED_LANGUAGES,
|
|
17
|
+
KWS_V2_COUNTRIES,
|
|
18
|
+
} from '../../../age-assurance/kws/const'
|
|
19
|
+
import {
|
|
20
|
+
KWSExternalPayloadTooLargeError,
|
|
21
|
+
KWSExternalPayloadVersion,
|
|
22
|
+
serializeKWSExternalPayloadV2,
|
|
23
|
+
} from '../../../age-assurance/kws/external-payload'
|
|
24
|
+
import { createEvent } from '../../../age-assurance/stash'
|
|
25
|
+
import { createLocationString } from '../../../age-assurance/util'
|
|
26
|
+
import { getClientUa } from '../../../kws/util'
|
|
27
|
+
|
|
28
|
+
export default function (server: Server, ctx: AppContext) {
|
|
29
|
+
server.app.bsky.ageassurance.begin({
|
|
30
|
+
auth: ctx.authVerifier.standard,
|
|
31
|
+
handler: async ({ auth, input, req }) => {
|
|
32
|
+
if (!ctx.kwsClient) {
|
|
33
|
+
throw new MethodNotImplementedError(
|
|
34
|
+
'This service is not configured to support age assurance.',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const actorDid = auth.credentials.iss
|
|
39
|
+
const actorInfo = await getAgeVerificationState(ctx, actorDid)
|
|
40
|
+
|
|
41
|
+
if (actorInfo?.ageAssuranceStatus) {
|
|
42
|
+
if (
|
|
43
|
+
actorInfo.ageAssuranceStatus.status !== 'unknown' &&
|
|
44
|
+
actorInfo.ageAssuranceStatus.status !== 'pending'
|
|
45
|
+
) {
|
|
46
|
+
throw new InvalidRequestError(
|
|
47
|
+
`Cannot initiate age assurance flow from current state: ${actorInfo.ageAssuranceStatus.status}`,
|
|
48
|
+
'InvalidInitiation',
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const attemptId = crypto.randomUUID()
|
|
54
|
+
const { email, language, countryCode, regionCode } = validateInput(
|
|
55
|
+
input.body,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
let externalPayload: string
|
|
59
|
+
try {
|
|
60
|
+
externalPayload = serializeKWSExternalPayloadV2({
|
|
61
|
+
version: KWSExternalPayloadVersion.V2,
|
|
62
|
+
actorDid,
|
|
63
|
+
attemptId,
|
|
64
|
+
countryCode,
|
|
65
|
+
regionCode,
|
|
66
|
+
})
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof KWSExternalPayloadTooLargeError) {
|
|
69
|
+
log.error({ err, actorDid }, err.message)
|
|
70
|
+
throw new InvalidRequestError(
|
|
71
|
+
'Age Assurance flow failed because DID is too long',
|
|
72
|
+
'DidTooLong',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
throw err
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* Determine if age assurance config exists for this region. The calling
|
|
80
|
+
* application should already have checked for this, so this is just a
|
|
81
|
+
* safeguard.
|
|
82
|
+
*/
|
|
83
|
+
const region = getAgeAssuranceRegionConfig(AGE_ASSURANCE_CONFIG, {
|
|
84
|
+
countryCode,
|
|
85
|
+
regionCode,
|
|
86
|
+
})
|
|
87
|
+
if (!region) {
|
|
88
|
+
const message = 'Age Assurance is not required in this region'
|
|
89
|
+
log.error({ actorDid, countryCode, regionCode }, message)
|
|
90
|
+
throw new InvalidRequestError(message, 'RegionNotSupported')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const location = createLocationString(countryCode, regionCode)
|
|
94
|
+
|
|
95
|
+
if (KWS_V2_COUNTRIES.has(region.countryCode)) {
|
|
96
|
+
// `age-verified` flow
|
|
97
|
+
await ctx.kwsClient.sendAgeVerifiedFlowEmail({
|
|
98
|
+
location,
|
|
99
|
+
email,
|
|
100
|
+
externalPayload,
|
|
101
|
+
language,
|
|
102
|
+
})
|
|
103
|
+
} else {
|
|
104
|
+
// `adult-verified` flow is what we've been using prior to `age-verified`
|
|
105
|
+
await ctx.kwsClient.sendAdultVerifiedFlowEmail({
|
|
106
|
+
location,
|
|
107
|
+
email,
|
|
108
|
+
externalPayload,
|
|
109
|
+
language,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const event = await createEvent(ctx, actorDid, {
|
|
114
|
+
attemptId,
|
|
115
|
+
email,
|
|
116
|
+
// Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
|
|
117
|
+
initIp: req.ip,
|
|
118
|
+
initUa: getClientUa(req),
|
|
119
|
+
status: 'pending',
|
|
120
|
+
access: 'unknown',
|
|
121
|
+
countryCode,
|
|
122
|
+
regionCode,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
encoding: 'application/json',
|
|
127
|
+
body: {
|
|
128
|
+
lastInitiatedAt: event.createdAt,
|
|
129
|
+
status: 'pending',
|
|
130
|
+
access: 'unknown',
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateInput({ email, language, ...rest }: InputSchema): InputSchema {
|
|
138
|
+
if (!isEmailValid(email) || isDisposableEmail(email)) {
|
|
139
|
+
throw new InvalidRequestError(
|
|
140
|
+
'This email address is not supported, please use a different email.',
|
|
141
|
+
'InvalidEmail',
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
email,
|
|
147
|
+
language: KWS_SUPPORTED_LANGUAGES.has(language) ? language : 'en',
|
|
148
|
+
...rest,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function getAgeVerificationState(
|
|
153
|
+
ctx: AppContext,
|
|
154
|
+
actorDid: string,
|
|
155
|
+
): Promise<ActorInfo | undefined> {
|
|
156
|
+
try {
|
|
157
|
+
const res = await ctx.dataplane.getActors({
|
|
158
|
+
dids: [actorDid],
|
|
159
|
+
returnAgeAssuranceForDids: [actorDid],
|
|
160
|
+
skipCacheForDids: [actorDid],
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return res.actors[0]
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AGE_ASSURANCE_CONFIG } from '../../../../api/age-assurance/const'
|
|
2
|
+
import { AppContext } from '../../../../context'
|
|
3
|
+
import { Server } from '../../../../lexicon'
|
|
4
|
+
|
|
5
|
+
export default function (server: Server, ctx: AppContext) {
|
|
6
|
+
server.app.bsky.ageassurance.getConfig({
|
|
7
|
+
auth: ctx.authVerifier.standardOptional,
|
|
8
|
+
handler: async () => {
|
|
9
|
+
return {
|
|
10
|
+
encoding: 'application/json',
|
|
11
|
+
body: AGE_ASSURANCE_CONFIG,
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { UpstreamFailureError } from '@atproto/xrpc-server'
|
|
2
|
+
import { AppContext } from '../../../../context'
|
|
3
|
+
import { Server } from '../../../../lexicon'
|
|
4
|
+
import { ActorInfo } from '../../../../proto/bsky_pb'
|
|
5
|
+
|
|
6
|
+
export default function (server: Server, ctx: AppContext) {
|
|
7
|
+
server.app.bsky.ageassurance.getState({
|
|
8
|
+
auth: ctx.authVerifier.standard,
|
|
9
|
+
handler: async ({ auth }) => {
|
|
10
|
+
const viewer = auth.credentials.iss
|
|
11
|
+
const actor = await getActorInfo(ctx, viewer)
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
encoding: 'application/json',
|
|
15
|
+
body: {
|
|
16
|
+
state: {
|
|
17
|
+
lastInitiatedAt:
|
|
18
|
+
actor.ageAssuranceStatus?.lastInitiatedAt
|
|
19
|
+
?.toDate()
|
|
20
|
+
.toISOString() || undefined,
|
|
21
|
+
status: actor.ageAssuranceStatus?.status || 'unknown',
|
|
22
|
+
access: actor.ageAssuranceStatus?.access || 'unknown',
|
|
23
|
+
},
|
|
24
|
+
metadata: {
|
|
25
|
+
accountCreatedAt:
|
|
26
|
+
actor.createdAt?.toDate().toISOString() || undefined,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getActorInfo = async (
|
|
35
|
+
ctx: AppContext,
|
|
36
|
+
actorDid: string,
|
|
37
|
+
): Promise<ActorInfo> => {
|
|
38
|
+
try {
|
|
39
|
+
const res = await ctx.dataplane.getActors({
|
|
40
|
+
dids: [actorDid],
|
|
41
|
+
returnAgeAssuranceForDids: [actorDid],
|
|
42
|
+
skipCacheForDids: [actorDid],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return res.actors[0]
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new UpstreamFailureError(
|
|
48
|
+
'Cannot get current age assurance state',
|
|
49
|
+
'GetAgeAssuranceStateFailed',
|
|
50
|
+
{ cause: err },
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/api/external.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express'
|
|
2
2
|
import { AppContext } from '../context'
|
|
3
|
+
import * as aaApi from './age-assurance'
|
|
3
4
|
import * as kwsApi from './kws'
|
|
4
5
|
|
|
5
6
|
export const createRouter = (ctx: AppContext): Router => {
|
|
@@ -7,6 +8,7 @@ export const createRouter = (ctx: AppContext): Router => {
|
|
|
7
8
|
|
|
8
9
|
if (ctx.kwsClient) {
|
|
9
10
|
router.use('/kws', kwsApi.createRouter(ctx))
|
|
11
|
+
router.use(aaApi.createRouter(ctx))
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
return router
|
package/src/api/index.ts
CHANGED
|
@@ -5,6 +5,9 @@ import getProfiles from './app/bsky/actor/getProfiles'
|
|
|
5
5
|
import getSuggestions from './app/bsky/actor/getSuggestions'
|
|
6
6
|
import searchActors from './app/bsky/actor/searchActors'
|
|
7
7
|
import searchActorsTypeahead from './app/bsky/actor/searchActorsTypeahead'
|
|
8
|
+
import aaBegin from './app/bsky/ageassurance/begin'
|
|
9
|
+
import aaGetConfig from './app/bsky/ageassurance/getConfig'
|
|
10
|
+
import aaGetState from './app/bsky/ageassurance/getState'
|
|
8
11
|
import createBookmark from './app/bsky/bookmark/createBookmark'
|
|
9
12
|
import deleteBookmark from './app/bsky/bookmark/deleteBookmark'
|
|
10
13
|
import getBookmarks from './app/bsky/bookmark/getBookmarks'
|
|
@@ -156,6 +159,9 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
156
159
|
getTaggedSuggestions(server, ctx)
|
|
157
160
|
getAgeAssuranceState(server, ctx)
|
|
158
161
|
initAgeAssurance(server, ctx)
|
|
162
|
+
aaGetConfig(server, ctx)
|
|
163
|
+
aaGetState(server, ctx)
|
|
164
|
+
aaBegin(server, ctx)
|
|
159
165
|
// com.atproto
|
|
160
166
|
getSubjectStatus(server, ctx)
|
|
161
167
|
updateSubjectStatus(server, ctx)
|
package/src/api/kws/api.ts
CHANGED
|
@@ -1,42 +1,41 @@
|
|
|
1
1
|
import express, { RequestHandler } from 'express'
|
|
2
2
|
import { httpLogger as log } from '../../logger'
|
|
3
|
+
import { AGE_ASSURANCE_CONFIG } from '../age-assurance/const'
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '
|
|
5
|
+
KWSExternalPayloadVersion,
|
|
6
|
+
parseKWSExternalPayloadV1WithV2Compat,
|
|
7
|
+
} from '../age-assurance/kws/external-payload'
|
|
8
|
+
import { createEvent } from '../age-assurance/stash'
|
|
9
|
+
import { computeAgeAssuranceAccessOrThrow } from '../age-assurance/util'
|
|
10
|
+
import { AppContextWithKwsClient } from './types'
|
|
9
11
|
import {
|
|
10
12
|
createStashEvent,
|
|
11
13
|
getClientUa,
|
|
12
|
-
parseExternalPayload,
|
|
13
14
|
parseStatus,
|
|
14
15
|
validateSignature,
|
|
15
16
|
} from './util'
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
function parseQueryParams(
|
|
18
19
|
ctx: AppContextWithKwsClient,
|
|
19
20
|
req: express.Request,
|
|
20
|
-
):
|
|
21
|
+
): {
|
|
22
|
+
status: string
|
|
23
|
+
externalPayload: string
|
|
24
|
+
} {
|
|
21
25
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
signature: req.query.signature,
|
|
26
|
-
status: req.query.status,
|
|
27
|
-
})
|
|
26
|
+
const status = String(req.query.status)
|
|
27
|
+
const externalPayload = String(req.query.externalPayload)
|
|
28
|
+
const signature = String(req.query.signature)
|
|
28
29
|
|
|
29
|
-
const data = `${intermediate.status}:${intermediate.externalPayload}`
|
|
30
30
|
validateSignature(
|
|
31
31
|
ctx.cfg.kws.verificationSecret,
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
`${status}:${externalPayload}`,
|
|
33
|
+
signature,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
|
-
|
|
38
|
-
externalPayload
|
|
39
|
-
status: parseStatus(intermediate.status),
|
|
37
|
+
status,
|
|
38
|
+
externalPayload,
|
|
40
39
|
}
|
|
41
40
|
} catch (err) {
|
|
42
41
|
throw new Error('Invalid KWS API request', { cause: err })
|
|
@@ -48,29 +47,51 @@ export const verificationHandler =
|
|
|
48
47
|
async (req: express.Request, res: express.Response) => {
|
|
49
48
|
let actorDid: string | undefined
|
|
50
49
|
try {
|
|
51
|
-
const query =
|
|
52
|
-
const {
|
|
53
|
-
externalPayload,
|
|
54
|
-
status: { verified },
|
|
55
|
-
} = query
|
|
50
|
+
const query = parseQueryParams(ctx, req)
|
|
51
|
+
const { verified } = parseStatus(query.status)
|
|
56
52
|
if (!verified) {
|
|
57
53
|
throw new Error(
|
|
58
54
|
'Unexpected KWS verification response call with unverified status',
|
|
59
55
|
)
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
const { actorDid: externalPayloadActorDid, attemptId } = externalPayload
|
|
63
|
-
actorDid = externalPayloadActorDid
|
|
64
58
|
// Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
|
|
65
59
|
const completeIp = req.ip
|
|
66
60
|
const completeUa = getClientUa(req)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
const externalPayload = parseKWSExternalPayloadV1WithV2Compat(
|
|
62
|
+
query.externalPayload,
|
|
63
|
+
)
|
|
64
|
+
actorDid = externalPayload.actorDid
|
|
65
|
+
|
|
66
|
+
if (externalPayload.version === KWSExternalPayloadVersion.V2) {
|
|
67
|
+
const { countryCode, regionCode, attemptId } = externalPayload
|
|
68
|
+
const { access } = computeAgeAssuranceAccessOrThrow(
|
|
69
|
+
AGE_ASSURANCE_CONFIG,
|
|
70
|
+
{
|
|
71
|
+
countryCode: countryCode,
|
|
72
|
+
regionCode: regionCode,
|
|
73
|
+
verifiedMinimumAge: 18, // `adult-verified` is 18+ only
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
await createEvent(ctx, actorDid, {
|
|
77
|
+
attemptId,
|
|
78
|
+
status: 'assured',
|
|
79
|
+
access,
|
|
80
|
+
countryCode,
|
|
81
|
+
regionCode,
|
|
82
|
+
completeIp,
|
|
83
|
+
completeUa,
|
|
84
|
+
})
|
|
85
|
+
} else {
|
|
86
|
+
await createStashEvent(ctx, {
|
|
87
|
+
actorDid,
|
|
88
|
+
attemptId: externalPayload.attemptId,
|
|
89
|
+
status: 'assured',
|
|
90
|
+
completeIp,
|
|
91
|
+
completeUa,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
return res
|
|
75
96
|
.status(302)
|
|
76
97
|
.setHeader(
|