@atproto/pds 0.4.220 → 0.4.221

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/auth-verifier.d.ts +1 -0
  3. package/dist/auth-verifier.d.ts.map +1 -1
  4. package/dist/auth-verifier.js +7 -13
  5. package/dist/auth-verifier.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/lexicons/app/bsky/actor/defs.defs.d.ts +1 -0
  11. package/dist/lexicons/app/bsky/actor/defs.defs.d.ts.map +1 -1
  12. package/dist/lexicons/app/bsky/actor/defs.defs.js +1 -0
  13. package/dist/lexicons/app/bsky/actor/defs.defs.js.map +1 -1
  14. package/dist/lexicons/chat/bsky/actor/defs.defs.d.ts +11 -3
  15. package/dist/lexicons/chat/bsky/actor/defs.defs.d.ts.map +1 -1
  16. package/dist/lexicons/chat/bsky/actor/defs.defs.js +6 -2
  17. package/dist/lexicons/chat/bsky/actor/defs.defs.js.map +1 -1
  18. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts +98 -23
  19. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts.map +1 -1
  20. package/dist/lexicons/chat/bsky/convo/defs.defs.js +34 -24
  21. package/dist/lexicons/chat/bsky/convo/defs.defs.js.map +1 -1
  22. package/dist/lexicons/chat/bsky/convo/getConvoMembers.d.ts +3 -0
  23. package/dist/lexicons/chat/bsky/convo/getConvoMembers.d.ts.map +1 -0
  24. package/dist/lexicons/chat/bsky/convo/getConvoMembers.defs.d.ts +26 -0
  25. package/dist/lexicons/chat/bsky/convo/getConvoMembers.defs.d.ts.map +1 -0
  26. package/dist/lexicons/chat/bsky/convo/getConvoMembers.defs.js +55 -0
  27. package/dist/lexicons/chat/bsky/convo/getConvoMembers.defs.js.map +1 -0
  28. package/dist/lexicons/chat/bsky/convo/getConvoMembers.js +45 -0
  29. package/dist/lexicons/chat/bsky/convo/getConvoMembers.js.map +1 -0
  30. package/dist/lexicons/chat/bsky/convo/getMessages.defs.d.ts +3 -0
  31. package/dist/lexicons/chat/bsky/convo/getMessages.defs.d.ts.map +1 -1
  32. package/dist/lexicons/chat/bsky/convo/getMessages.defs.js +2 -0
  33. package/dist/lexicons/chat/bsky/convo/getMessages.defs.js.map +1 -1
  34. package/dist/lexicons/chat/bsky/convo.d.ts +1 -0
  35. package/dist/lexicons/chat/bsky/convo.d.ts.map +1 -1
  36. package/dist/lexicons/chat/bsky/convo.js +2 -1
  37. package/dist/lexicons/chat/bsky/convo.js.map +1 -1
  38. package/dist/lexicons/chat/bsky/group/addMembers.defs.d.ts +3 -0
  39. package/dist/lexicons/chat/bsky/group/addMembers.defs.d.ts.map +1 -1
  40. package/dist/lexicons/chat/bsky/group/addMembers.defs.js +2 -0
  41. package/dist/lexicons/chat/bsky/group/addMembers.defs.js.map +1 -1
  42. package/package.json +11 -12
  43. package/src/auth-verifier.ts +3 -11
  44. package/src/index.ts +5 -1
  45. package/tests/auth.test.ts +3 -1
  46. package/tests/entryway-mock.ts +317 -0
  47. package/tests/entryway.test.ts +18 -100
  48. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,5 +1,6 @@
1
1
  import { l } from '@atproto/lex';
2
2
  import * as ConvoDefs from '../convo/defs.defs.js';
3
+ import * as ActorDefs from '../actor/defs.defs.js';
3
4
  declare const $nsid = "chat.bsky.group.addMembers";
4
5
  export { $nsid };
5
6
  /** [NOTE: This is under active development and should be considered unstable while this note is here]. Adds members to a group. The members are added in 'request' status, so they have to accept it. This creates convo memberships. */
@@ -10,6 +11,7 @@ declare const main: l.Procedure<"chat.bsky.group.addMembers", l.ParamsSchema<{}>
10
11
  }>>;
11
12
  }>>, l.Payload<"application/json", l.ObjectSchema<{
12
13
  convo: l.RefSchema<l.Validator<ConvoDefs.ConvoView, ConvoDefs.ConvoView>>;
14
+ addedMembers: l.OptionalSchema<l.ArraySchema<l.RefSchema<l.Validator<ActorDefs.ProfileViewBasic, ActorDefs.ProfileViewBasic>>>>;
13
15
  }>>, readonly ["AccountSuspended", "BlockedActor", "GroupInvitesDisabled", "ConvoLocked", "InsufficientRole", "InvalidConvo", "MemberLimitReached", "NotFollowedBySender", "RecipientNotFound"]>;
14
16
  export { main };
15
17
  export type $Params = l.InferMethodParams<typeof main>;
@@ -24,5 +26,6 @@ export declare const $lxm: "chat.bsky.group.addMembers", $params: l.ParamsSchema
24
26
  }>>;
25
27
  }>>, $output: l.Payload<"application/json", l.ObjectSchema<{
26
28
  convo: l.RefSchema<l.Validator<ConvoDefs.ConvoView, ConvoDefs.ConvoView>>;
29
+ addedMembers: l.OptionalSchema<l.ArraySchema<l.RefSchema<l.Validator<ActorDefs.ProfileViewBasic, ActorDefs.ProfileViewBasic>>>>;
27
30
  }>>;
28
31
  //# sourceMappingURL=addMembers.defs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"addMembers.defs.d.ts","sourceRoot":"","sources":["../../../../../src/lexicons/chat/bsky/group/addMembers.defs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAChC,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAA;AAElD,QAAA,MAAM,KAAK,+BAA+B,CAAA;AAE1C,OAAO,EAAE,KAAK,EAAE,CAAA;AAEhB,yOAAyO;AACzO,QAAA,MAAM,IAAI;;;;;;;gMAqBT,CAAA;AACD,OAAO,EAAE,IAAI,EAAE,CAAA;AAEf,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,iBAAiB,CAAC,OAAO,IAAI,CAAC,CAAA;AACtD,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,gBAAgB,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAA;AACzE,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,oBAAoB,CAC/D,OAAO,IAAI,EACX,CAAC,CACF,CAAA;AACD,MAAM,MAAM,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,iBAAiB,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAA;AAC3E,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,qBAAqB,CACjE,OAAO,IAAI,EACX,CAAC,CACF,CAAA;AAED,eAAO,MAAM,IAAI,8BAAY,EAC3B,OAAO,oBAAkB,EACzB,MAAM;;;;;GAAa,EACnB,OAAO;;GAAc,CAAA"}
1
+ {"version":3,"file":"addMembers.defs.d.ts","sourceRoot":"","sources":["../../../../../src/lexicons/chat/bsky/group/addMembers.defs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAChC,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAA;AAClD,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAA;AAElD,QAAA,MAAM,KAAK,+BAA+B,CAAA;AAE1C,OAAO,EAAE,KAAK,EAAE,CAAA;AAEhB,yOAAyO;AACzO,QAAA,MAAM,IAAI;;;;;;;;gMA4BT,CAAA;AACD,OAAO,EAAE,IAAI,EAAE,CAAA;AAEf,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,iBAAiB,CAAC,OAAO,IAAI,CAAC,CAAA;AACtD,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,gBAAgB,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAA;AACzE,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,oBAAoB,CAC/D,OAAO,IAAI,EACX,CAAC,CACF,CAAA;AACD,MAAM,MAAM,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,iBAAiB,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAA;AAC3E,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,qBAAqB,CACjE,OAAO,IAAI,EACX,CAAC,CACF,CAAA;AAED,eAAO,MAAM,IAAI,8BAAY,EAC3B,OAAO,oBAAkB,EACzB,MAAM;;;;;GAAa,EACnB,OAAO;;;GAAc,CAAA"}
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.$output = exports.$input = exports.$params = exports.$lxm = exports.main = exports.$nsid = void 0;
40
40
  const lex_1 = require("@atproto/lex");
41
41
  const ConvoDefs = __importStar(require("../convo/defs.defs.js"));
42
+ const ActorDefs = __importStar(require("../actor/defs.defs.js"));
42
43
  const $nsid = 'chat.bsky.group.addMembers';
43
44
  exports.$nsid = $nsid;
44
45
  /** [NOTE: This is under active development and should be considered unstable while this note is here]. Adds members to a group. The members are added in 'request' status, so they have to accept it. This creates convo memberships. */
@@ -47,6 +48,7 @@ const main = lex_1.l.procedure($nsid, lex_1.l.params(), lex_1.l.jsonPayload({
47
48
  members: lex_1.l.array(lex_1.l.string({ format: 'did' }), { minLength: 1 }),
48
49
  }), lex_1.l.jsonPayload({
49
50
  convo: lex_1.l.ref((() => ConvoDefs.convoView)),
51
+ addedMembers: lex_1.l.optional(lex_1.l.array(lex_1.l.ref((() => ActorDefs.profileViewBasic)))),
50
52
  }), [
51
53
  'AccountSuspended',
52
54
  'BlockedActor',
@@ -1 +1 @@
1
- {"version":3,"file":"addMembers.defs.js","sourceRoot":"","sources":["../../../../../src/lexicons/chat/bsky/group/addMembers.defs.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,sCAAgC;AAChC,iEAAkD;AAElD,MAAM,KAAK,GAAG,4BAA4B,CAAA;AAEjC,sBAAK;AAEd,yOAAyO;AACzO,MAAM,IAAI,GAAG,OAAC,CAAC,SAAS,CACtB,KAAK,EACL,OAAC,CAAC,MAAM,EAAE,EACV,OAAC,CAAC,WAAW,CAAC;IACZ,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;IACnB,OAAO,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;CAChE,CAAC,EACF,OAAC,CAAC,WAAW,CAAC;IACZ,KAAK,EAAE,OAAC,CAAC,GAAG,CAAsB,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,SAAS,CAAQ,CAAC;CACtE,CAAC,EACF;IACE,kBAAkB;IAClB,cAAc;IACd,sBAAsB;IACtB,aAAa;IACb,kBAAkB;IAClB,cAAc;IACd,oBAAoB;IACpB,qBAAqB;IACrB,mBAAmB;CACpB,CACF,CAAA;AACQ,oBAAI;AAcA,QAAA,IAAI,GAAG,IAAI,CAAC,IAAI,EAC3B,QAAA,OAAO,GAAG,IAAI,CAAC,UAAU,EACzB,QAAA,MAAM,GAAG,IAAI,CAAC,KAAK,EACnB,QAAA,OAAO,GAAG,IAAI,CAAC,MAAM,CAAA","sourcesContent":["/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nimport { l } from '@atproto/lex'\nimport * as ConvoDefs from '../convo/defs.defs.js'\n\nconst $nsid = 'chat.bsky.group.addMembers'\n\nexport { $nsid }\n\n/** [NOTE: This is under active development and should be considered unstable while this note is here]. Adds members to a group. The members are added in 'request' status, so they have to accept it. This creates convo memberships. */\nconst main = l.procedure(\n $nsid,\n l.params(),\n l.jsonPayload({\n convoId: l.string(),\n members: l.array(l.string({ format: 'did' }), { minLength: 1 }),\n }),\n l.jsonPayload({\n convo: l.ref<ConvoDefs.ConvoView>((() => ConvoDefs.convoView) as any),\n }),\n [\n 'AccountSuspended',\n 'BlockedActor',\n 'GroupInvitesDisabled',\n 'ConvoLocked',\n 'InsufficientRole',\n 'InvalidConvo',\n 'MemberLimitReached',\n 'NotFollowedBySender',\n 'RecipientNotFound',\n ],\n)\nexport { main }\n\nexport type $Params = l.InferMethodParams<typeof main>\nexport type $Input<B = l.BinaryData> = l.InferMethodInput<typeof main, B>\nexport type $InputBody<B = l.BinaryData> = l.InferMethodInputBody<\n typeof main,\n B\n>\nexport type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>\nexport type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<\n typeof main,\n B\n>\n\nexport const $lxm = main.nsid,\n $params = main.parameters,\n $input = main.input,\n $output = main.output\n"]}
1
+ {"version":3,"file":"addMembers.defs.js","sourceRoot":"","sources":["../../../../../src/lexicons/chat/bsky/group/addMembers.defs.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,sCAAgC;AAChC,iEAAkD;AAClD,iEAAkD;AAElD,MAAM,KAAK,GAAG,4BAA4B,CAAA;AAEjC,sBAAK;AAEd,yOAAyO;AACzO,MAAM,IAAI,GAAG,OAAC,CAAC,SAAS,CACtB,KAAK,EACL,OAAC,CAAC,MAAM,EAAE,EACV,OAAC,CAAC,WAAW,CAAC;IACZ,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE;IACnB,OAAO,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;CAChE,CAAC,EACF,OAAC,CAAC,WAAW,CAAC;IACZ,KAAK,EAAE,OAAC,CAAC,GAAG,CAAsB,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,SAAS,CAAQ,CAAC;IACrE,YAAY,EAAE,OAAC,CAAC,QAAQ,CACtB,OAAC,CAAC,KAAK,CACL,OAAC,CAAC,GAAG,CACH,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,gBAAgB,CAAQ,CAC1C,CACF,CACF;CACF,CAAC,EACF;IACE,kBAAkB;IAClB,cAAc;IACd,sBAAsB;IACtB,aAAa;IACb,kBAAkB;IAClB,cAAc;IACd,oBAAoB;IACpB,qBAAqB;IACrB,mBAAmB;CACpB,CACF,CAAA;AACQ,oBAAI;AAcA,QAAA,IAAI,GAAG,IAAI,CAAC,IAAI,EAC3B,QAAA,OAAO,GAAG,IAAI,CAAC,UAAU,EACzB,QAAA,MAAM,GAAG,IAAI,CAAC,KAAK,EACnB,QAAA,OAAO,GAAG,IAAI,CAAC,MAAM,CAAA","sourcesContent":["/*\n * THIS FILE WAS GENERATED BY \"@atproto/lex\". DO NOT EDIT.\n */\n\nimport { l } from '@atproto/lex'\nimport * as ConvoDefs from '../convo/defs.defs.js'\nimport * as ActorDefs from '../actor/defs.defs.js'\n\nconst $nsid = 'chat.bsky.group.addMembers'\n\nexport { $nsid }\n\n/** [NOTE: This is under active development and should be considered unstable while this note is here]. Adds members to a group. The members are added in 'request' status, so they have to accept it. This creates convo memberships. */\nconst main = l.procedure(\n $nsid,\n l.params(),\n l.jsonPayload({\n convoId: l.string(),\n members: l.array(l.string({ format: 'did' }), { minLength: 1 }),\n }),\n l.jsonPayload({\n convo: l.ref<ConvoDefs.ConvoView>((() => ConvoDefs.convoView) as any),\n addedMembers: l.optional(\n l.array(\n l.ref<ActorDefs.ProfileViewBasic>(\n (() => ActorDefs.profileViewBasic) as any,\n ),\n ),\n ),\n }),\n [\n 'AccountSuspended',\n 'BlockedActor',\n 'GroupInvitesDisabled',\n 'ConvoLocked',\n 'InsufficientRole',\n 'InvalidConvo',\n 'MemberLimitReached',\n 'NotFollowedBySender',\n 'RecipientNotFound',\n ],\n)\nexport { main }\n\nexport type $Params = l.InferMethodParams<typeof main>\nexport type $Input<B = l.BinaryData> = l.InferMethodInput<typeof main, B>\nexport type $InputBody<B = l.BinaryData> = l.InferMethodInputBody<\n typeof main,\n B\n>\nexport type $Output<B = l.BinaryData> = l.InferMethodOutput<typeof main, B>\nexport type $OutputBody<B = l.BinaryData> = l.InferMethodOutputBody<\n typeof main,\n B\n>\n\nexport const $lxm = main.nsid,\n $params = main.parameters,\n $input = main.input,\n $output = main.output\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/pds",
3
- "version": "0.4.220",
3
+ "version": "0.4.221",
4
4
  "license": "MIT",
5
5
  "description": "Reference implementation of atproto Personal Data Server (PDS)",
6
6
  "keywords": [
@@ -45,27 +45,26 @@
45
45
  "undici": "^6.19.8",
46
46
  "zod": "^3.23.8",
47
47
  "@atproto-labs/fetch-node": "^0.2.0",
48
- "@atproto-labs/simple-store-redis": "^0.0.1",
49
- "@atproto-labs/simple-store-memory": "^0.1.4",
50
48
  "@atproto-labs/simple-store": "^0.3.0",
49
+ "@atproto-labs/simple-store-memory": "^0.1.4",
50
+ "@atproto-labs/simple-store-redis": "^0.0.1",
51
51
  "@atproto-labs/xrpc-utils": "^0.0.24",
52
+ "@atproto/aws": "^0.2.32",
52
53
  "@atproto/common": "^0.5.16",
53
54
  "@atproto/crypto": "^0.4.5",
54
55
  "@atproto/identity": "^0.4.12",
55
56
  "@atproto/lex": "^0.0.25",
56
- "@atproto/aws": "^0.2.32",
57
57
  "@atproto/lex-cbor": "^0.0.16",
58
- "@atproto/lex-json": "^0.0.16",
59
58
  "@atproto/lex-data": "^0.0.15",
59
+ "@atproto/lex-json": "^0.0.16",
60
+ "@atproto/oauth-provider": "^0.16.3",
60
61
  "@atproto/oauth-scopes": "^0.3.2",
61
- "@atproto/oauth-provider": "^0.16.1",
62
- "@atproto/xrpc": "^0.7.7",
62
+ "@atproto/repo": "^0.9.1",
63
63
  "@atproto/syntax": "^0.5.4",
64
- "@atproto/xrpc-server": "^0.10.20",
65
- "@atproto/repo": "^0.9.1"
64
+ "@atproto/xrpc": "^0.7.7",
65
+ "@atproto/xrpc-server": "^0.10.20"
66
66
  },
67
67
  "devDependencies": {
68
- "@atproto/pds-entryway": "npm:@atproto/pds@0.3.0-entryway.3",
69
68
  "@did-plc/server": "^0.0.1",
70
69
  "@types/cors": "^2.8.12",
71
70
  "@types/express": "^4.17.13",
@@ -80,8 +79,8 @@
80
79
  "ts-node": "^10.8.2",
81
80
  "typescript": "^5.6.3",
82
81
  "ws": "^8.12.0",
83
- "@atproto/api": "^0.19.9",
84
- "@atproto/bsky": "^0.0.226",
82
+ "@atproto/api": "^0.19.11",
83
+ "@atproto/bsky": "^0.0.227",
85
84
  "@atproto/lex-document": "^0.0.20",
86
85
  "@atproto/oauth-client-browser-example": "^0.0.10"
87
86
  },
@@ -465,8 +465,8 @@ export class AuthVerifier {
465
465
  throw new AuthRequiredError(undefined, 'AuthMissing')
466
466
  }
467
467
 
468
- const { payload, protectedHeader } = await jose
469
- .jwtVerify(token, this._jwtKey, { ...options, typ: undefined })
468
+ const { payload } = await jose
469
+ .jwtVerify(token, this._jwtKey, options)
470
470
  .catch((cause) => {
471
471
  if (cause instanceof jose.errors.JWTExpired) {
472
472
  throw new InvalidRequestError('Token has expired', 'ExpiredToken', {
@@ -481,14 +481,6 @@ export class AuthVerifier {
481
481
  }
482
482
  })
483
483
 
484
- // @NOTE: the "typ" is now set in production environments, so we should be
485
- // able to safely check it through jose.jwtVerify(). However, tests depend
486
- // on @atproto/pds-entryway which does not set "typ" in the access tokens.
487
- // For that reason, we still allow it to be missing.
488
- if (protectedHeader.typ && options.typ !== protectedHeader.typ) {
489
- throw new InvalidRequestError('Invalid token type', 'InvalidToken')
490
- }
491
-
492
484
  const { sub, aud, scope, lxm, cnf, jti } = payload
493
485
 
494
486
  if (typeof lxm !== 'undefined') {
@@ -634,7 +626,7 @@ const extractAuthType = (req: IncomingMessage): AuthType | null => {
634
626
  return type
635
627
  }
636
628
 
637
- const bearerTokenFromReq = (req: IncomingMessage) => {
629
+ export const bearerTokenFromReq = (req: IncomingMessage) => {
638
630
  const [type, token] = parseAuthorizationHeader(req)
639
631
  return type === AuthType.BEARER ? token : null
640
632
  }
package/src/index.ts CHANGED
@@ -32,7 +32,11 @@ import compression from './util/compression'
32
32
  import * as wellKnown from './well-known'
33
33
 
34
34
  export * from './lexicons.js'
35
- export { createSecretKeyObject } from './auth-verifier'
35
+ export {
36
+ bearerTokenFromReq,
37
+ createPublicKeyObject,
38
+ createSecretKeyObject,
39
+ } from './auth-verifier'
36
40
  export * from './config'
37
41
  export { AppContext } from './context'
38
42
  export { Database } from './db'
@@ -331,7 +331,9 @@ describe('auth', () => {
331
331
  password: 'password',
332
332
  })
333
333
  const refreshWithAccess = refreshSession(account.accessJwt)
334
- await expect(refreshWithAccess).rejects.toThrow('Invalid token type')
334
+ await expect(refreshWithAccess).rejects.toThrow(
335
+ 'Token could not be verified',
336
+ )
335
337
  })
336
338
 
337
339
  it('expired refresh token cannot be used to refresh a session.', async () => {
@@ -0,0 +1,317 @@
1
+ import { createPrivateKey } from 'node:crypto'
2
+ import * as http from 'node:http'
3
+ import * as plcLib from '@did-plc/lib'
4
+ import { HttpTerminator, createHttpTerminator } from 'http-terminator'
5
+ import * as jose from 'jose'
6
+ import KeyEncoder from 'key-encoder'
7
+ import * as ui8 from 'uint8arrays'
8
+ import { AtpAgent } from '@atproto/api'
9
+ import { getVerificationMaterial } from '@atproto/common'
10
+ import { Secp256k1Keypair, randomStr } from '@atproto/crypto'
11
+ import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity'
12
+ import { DidString, HandleString } from '@atproto/syntax'
13
+ import {
14
+ AuthRequiredError,
15
+ createServer,
16
+ parseReqNsid,
17
+ verifyJwt as verifyServiceJwt,
18
+ } from '@atproto/xrpc-server'
19
+ import { bearerTokenFromReq, createPublicKeyObject } from '../src/auth-verifier'
20
+ import { com } from '../src/lexicons/index.js'
21
+
22
+ interface Account {
23
+ did: DidString
24
+ handle: HandleString
25
+ email?: string
26
+ }
27
+
28
+ interface MockEntrywayOpts {
29
+ port: number
30
+ serviceDid: string
31
+ plcUrl: string
32
+ pdsUrl: string
33
+ pdsDid: string
34
+ adminPassword: string
35
+ jwtSigningKey: Secp256k1Keypair
36
+ plcRotationKey: Secp256k1Keypair
37
+ }
38
+
39
+ type AccessAuthResult = { credentials: { did: string; type: 'access' } }
40
+ type ServiceAuthResult = { credentials: { did: string; type: 'service' } }
41
+
42
+ export class MockEntryway {
43
+ public url: string
44
+ public serviceDid: string
45
+ public plcRotationKey: Secp256k1Keypair
46
+ public idResolver: IdResolver
47
+
48
+ private server: http.Server
49
+ private terminator: HttpTerminator
50
+ private accounts = new Map<string, Account>()
51
+
52
+ private constructor(
53
+ server: http.Server,
54
+ terminator: HttpTerminator,
55
+ idResolver: IdResolver,
56
+ opts: MockEntrywayOpts,
57
+ ) {
58
+ this.server = server
59
+ this.terminator = terminator
60
+ this.url = `http://localhost:${opts.port}`
61
+ this.serviceDid = opts.serviceDid
62
+ this.plcRotationKey = opts.plcRotationKey
63
+ this.idResolver = idResolver
64
+ }
65
+
66
+ static async create(opts: MockEntrywayOpts): Promise<MockEntryway> {
67
+ const keyEncoder = new KeyEncoder('secp256k1')
68
+ const privateKeyHex = ui8.toString(await opts.jwtSigningKey.export(), 'hex')
69
+ const privatePem = keyEncoder.encodePrivate(privateKeyHex, 'raw', 'pem')
70
+ const jwtPrivateKey = createPrivateKey({ format: 'pem', key: privatePem })
71
+ const jwtPublicKey = createPublicKeyObject(
72
+ opts.jwtSigningKey.publicKeyStr('hex'),
73
+ )
74
+
75
+ const plcClient = new plcLib.Client(opts.plcUrl)
76
+ const pdsAgent = new AtpAgent({ service: opts.pdsUrl })
77
+ const idResolver = new IdResolver({ plcUrl: opts.plcUrl })
78
+
79
+ const accounts = new Map<string, Account>()
80
+
81
+ const getSigningKey = async (
82
+ iss: string,
83
+ forceRefresh: boolean,
84
+ ): Promise<string> => {
85
+ const [did, serviceId] = iss.split('#')
86
+ if (serviceId) {
87
+ throw new AuthRequiredError('no service id expected in iss claim')
88
+ }
89
+ const didDoc = await idResolver.did.resolve(did, forceRefresh)
90
+ if (!didDoc) {
91
+ throw new AuthRequiredError(`could not resolve did: ${did}`)
92
+ }
93
+ const parsedKey = getVerificationMaterial(didDoc, 'atproto')
94
+ if (!parsedKey) {
95
+ throw new AuthRequiredError('missing or bad key in did doc')
96
+ }
97
+ const didKey = getDidKeyFromMultibase(parsedKey)
98
+ if (!didKey) {
99
+ throw new AuthRequiredError('missing or bad key in did doc')
100
+ }
101
+ return didKey
102
+ }
103
+
104
+ const bearerToken = (req: http.IncomingMessage): string => {
105
+ const token = bearerTokenFromReq(req)
106
+ if (!token) {
107
+ throw new AuthRequiredError('missing bearer token')
108
+ }
109
+ return token
110
+ }
111
+
112
+ // Auth: verify user access token (typ: 'at+jwt') signed by entryway
113
+ const accessAuth = async ({
114
+ req,
115
+ }: {
116
+ req: http.IncomingMessage
117
+ }): Promise<AccessAuthResult> => {
118
+ try {
119
+ const token = bearerToken(req)
120
+ const { protectedHeader, payload } = await jose.jwtVerify(
121
+ token,
122
+ jwtPublicKey,
123
+ )
124
+ if (protectedHeader.typ !== 'at+jwt') {
125
+ throw new AuthRequiredError('expected typ: at+jwt')
126
+ }
127
+ if (!payload.sub) {
128
+ throw new AuthRequiredError('missing sub in token')
129
+ }
130
+ return { credentials: { did: payload.sub, type: 'access' } }
131
+ } catch (err) {
132
+ console.log(err)
133
+ throw err
134
+ }
135
+ }
136
+
137
+ // Auth: verify service auth token from PDS (no typ / typ !== 'at+jwt')
138
+ const serviceAuth = async ({
139
+ req,
140
+ }: {
141
+ req: http.IncomingMessage
142
+ }): Promise<ServiceAuthResult> => {
143
+ try {
144
+ const token = bearerToken(req)
145
+ const { typ } = jose.decodeProtectedHeader(token)
146
+ if (typ === 'at+jwt') {
147
+ throw new AuthRequiredError(
148
+ 'expected service auth: typ must not be at+jwt',
149
+ )
150
+ }
151
+ const nsid = parseReqNsid(req)
152
+ const payload = await verifyServiceJwt(
153
+ token,
154
+ opts.serviceDid,
155
+ nsid,
156
+ getSigningKey,
157
+ )
158
+ return { credentials: { did: payload.iss, type: 'service' } }
159
+ } catch (err) {
160
+ console.log(err)
161
+ throw err
162
+ }
163
+ }
164
+
165
+ // Auth: accept either access token or service auth
166
+ const accessOrServiceAuth = async ({
167
+ req,
168
+ }: {
169
+ req: http.IncomingMessage
170
+ }): Promise<AccessAuthResult | ServiceAuthResult> => {
171
+ const token = bearerToken(req)
172
+ const { typ } = jose.decodeProtectedHeader(token)
173
+ if (typ === 'at+jwt') {
174
+ return accessAuth({ req })
175
+ }
176
+ return serviceAuth({ req })
177
+ }
178
+
179
+ const server = createServer()
180
+
181
+ server.add(com.atproto.server.createAccount, {
182
+ handler: async ({ input }) => {
183
+ const { email, handle } = input.body
184
+
185
+ // Reserve a signing key on the PDS
186
+ const {
187
+ data: { signingKey },
188
+ } = await pdsAgent.com.atproto.server.reserveSigningKey({})
189
+
190
+ // Create PLC operation
191
+ const plcCreate = await plcLib.createOp({
192
+ signingKey,
193
+ rotationKeys: [opts.plcRotationKey.did()],
194
+ handle,
195
+ pds: opts.pdsUrl,
196
+ signer: opts.plcRotationKey,
197
+ })
198
+
199
+ // Create account on PDS (no auth needed — userServiceAuthOptional)
200
+ await pdsAgent.com.atproto.server.createAccount({
201
+ did: plcCreate.did,
202
+ handle,
203
+ plcOp: plcCreate.op,
204
+ })
205
+
206
+ // Store account in memory
207
+ accounts.set(plcCreate.did, {
208
+ did: plcCreate.did as DidString,
209
+ handle,
210
+ email,
211
+ })
212
+
213
+ // Sign access + refresh JWTs
214
+ const now = Math.floor(Date.now() / 1000)
215
+ const accessJwt = await new jose.SignJWT({
216
+ scope: 'com.atproto.access',
217
+ })
218
+ .setProtectedHeader({ alg: 'ES256K', typ: 'at+jwt' })
219
+ .setSubject(plcCreate.did)
220
+ .setAudience(opts.pdsDid)
221
+ .setIssuedAt(now)
222
+ .setExpirationTime(now + 60 * 60)
223
+ .setJti(randomStr(16, 'base32'))
224
+ .sign(jwtPrivateKey)
225
+
226
+ const refreshJwt = await new jose.SignJWT({
227
+ scope: 'com.atproto.refresh',
228
+ })
229
+ .setProtectedHeader({ alg: 'ES256K', typ: 'at+jwt' })
230
+ .setSubject(plcCreate.did)
231
+ .setAudience(opts.pdsDid)
232
+ .setIssuedAt(now)
233
+ .setExpirationTime(now + 90 * 24 * 60 * 60)
234
+ .setJti(randomStr(16, 'base32'))
235
+ .sign(jwtPrivateKey)
236
+
237
+ return {
238
+ encoding: 'application/json' as const,
239
+ body: {
240
+ did: plcCreate.did as DidString,
241
+ handle,
242
+ accessJwt,
243
+ refreshJwt,
244
+ },
245
+ }
246
+ },
247
+ })
248
+
249
+ server.add(com.atproto.server.getSession, {
250
+ auth: accessOrServiceAuth,
251
+ handler: async ({ auth }) => {
252
+ const account = accounts.get(auth.credentials.did)
253
+ if (!account) {
254
+ throw new Error(
255
+ `Could not find account for DID: ${auth.credentials.did}`,
256
+ )
257
+ }
258
+ return {
259
+ encoding: 'application/json' as const,
260
+ body: {
261
+ did: account.did,
262
+ handle: account.handle,
263
+ email: account.email,
264
+ emailConfirmed: false,
265
+ },
266
+ }
267
+ },
268
+ })
269
+
270
+ server.add(com.atproto.identity.updateHandle, {
271
+ auth: serviceAuth,
272
+ handler: async ({ auth, input }) => {
273
+ // The PDS sends { did, handle } where did is the target user
274
+ const body = input.body as typeof input.body & { did?: string }
275
+ const targetDid = body.did || auth.credentials.did
276
+ const newHandle = body.handle
277
+
278
+ // Update handle in PLC
279
+ await plcClient.updateHandle(targetDid, opts.plcRotationKey, newHandle)
280
+
281
+ // Update in-memory account
282
+ const account = accounts.get(targetDid)
283
+ if (account) {
284
+ account.handle = newHandle
285
+ }
286
+
287
+ // Notify PDS via admin endpoint
288
+ const adminAuth = Buffer.from(`admin:${opts.adminPassword}`).toString(
289
+ 'base64',
290
+ )
291
+ await pdsAgent.com.atproto.admin.updateAccountHandle(
292
+ { did: targetDid, handle: newHandle },
293
+ {
294
+ headers: { authorization: `Basic ${adminAuth}` },
295
+ encoding: 'application/json',
296
+ },
297
+ )
298
+ },
299
+ })
300
+
301
+ const httpServer = server.listen(opts.port)
302
+ const terminator = createHttpTerminator({ server: httpServer })
303
+
304
+ const instance = new MockEntryway(httpServer, terminator, idResolver, opts)
305
+ instance.accounts = accounts
306
+
307
+ return instance
308
+ }
309
+
310
+ getAccount(did: string): Account | undefined {
311
+ return this.accounts.get(did)
312
+ }
313
+
314
+ async destroy(): Promise<void> {
315
+ await this.terminator.terminate()
316
+ }
317
+ }
@@ -1,22 +1,17 @@
1
1
  import assert from 'node:assert'
2
- import * as os from 'node:os'
3
- import * as path from 'node:path'
4
2
  import * as plcLib from '@did-plc/lib'
5
3
  import getPort from 'get-port'
6
- import { decodeJwt } from 'jose'
7
- import * as ui8 from 'uint8arrays'
8
4
  import { AtpAgent } from '@atproto/api'
9
- import { Secp256k1Keypair, randomStr } from '@atproto/crypto'
5
+ import { Secp256k1Keypair } from '@atproto/crypto'
10
6
  import { SeedClient, TestPds, TestPlc, mockResolvers } from '@atproto/dev-env'
11
7
  import { isDidString } from '@atproto/lex'
12
- import * as pdsEntryway from '@atproto/pds-entryway'
13
8
  import { DidString } from '@atproto/syntax'
14
- import { parseReqNsid } from '@atproto/xrpc-server'
9
+ import { MockEntryway } from './entryway-mock'
15
10
 
16
11
  describe('entryway', () => {
17
12
  let plc: TestPlc
18
13
  let pds: TestPds
19
- let entryway: pdsEntryway.PDS
14
+ let entryway: MockEntryway
20
15
  let pdsAgent: AtpAgent
21
16
  let entrywayAgent: AtpAgent
22
17
  let alice: DidString
@@ -30,7 +25,7 @@ describe('entryway', () => {
30
25
  pds = await TestPds.create({
31
26
  entrywayUrl: `http://localhost:${entrywayPort}`,
32
27
  entrywayDid: 'did:example:entryway',
33
- entrywayJwtVerifyKeyK256PublicKeyHex: getPublicHex(jwtSigningKey),
28
+ entrywayJwtVerifyKeyK256PublicKeyHex: jwtSigningKey.publicKeyStr('hex'),
34
29
  entrywayPlcRotationKey: plcRotationKey.did(),
35
30
  adminPassword: 'admin-pass',
36
31
  serviceHandleDomains: [],
@@ -38,29 +33,21 @@ describe('entryway', () => {
38
33
  serviceDid: 'did:example:pds',
39
34
  inviteRequired: false,
40
35
  })
41
- entryway = await createEntryway({
42
- dbPostgresSchema: 'entryway',
36
+ entryway = await MockEntryway.create({
43
37
  port: entrywayPort,
44
- adminPassword: 'admin-pass',
45
- jwtSigningKeyK256PrivateKeyHex: await getPrivateHex(jwtSigningKey),
46
- plcRotationKeyK256PrivateKeyHex: await getPrivateHex(plcRotationKey),
47
- inviteRequired: false,
48
38
  serviceDid: 'did:example:entryway',
49
- didPlcUrl: plc.url,
39
+ plcUrl: plc.url,
40
+ pdsUrl: pds.url,
41
+ pdsDid: 'did:example:pds',
42
+ adminPassword: 'admin-pass',
43
+ jwtSigningKey,
44
+ plcRotationKey,
50
45
  })
51
46
  mockResolvers(pds.ctx.idResolver, pds)
52
- mockResolvers(entryway.ctx.idResolver, pds)
53
- await entryway.ctx.db.db
54
- .insertInto('pds')
55
- .values({
56
- did: pds.ctx.cfg.service.did,
57
- host: new URL(pds.ctx.cfg.service.publicUrl).host,
58
- weight: 1,
59
- })
60
- .execute()
47
+ mockResolvers(entryway.idResolver, pds)
61
48
  pdsAgent = pds.getAgent()
62
49
  entrywayAgent = new AtpAgent({
63
- service: entryway.ctx.cfg.service.publicUrl,
50
+ service: entryway.url,
64
51
  })
65
52
  })
66
53
 
@@ -110,9 +97,7 @@ describe('entryway', () => {
110
97
  const doc = await pds.ctx.idResolver.did.resolve(alice)
111
98
  const handleToDid = await pds.ctx.idResolver.handle.resolve('alice2.test')
112
99
  const accountFromPds = await pds.ctx.accountManager.getAccount(alice)
113
- const accountFromEntryway = await entryway.ctx.services
114
- .account(entryway.ctx.db)
115
- .getAccount(alice)
100
+ const accountFromEntryway = entryway.getAccount(alice)
116
101
  expect(doc?.alsoKnownAs).toEqual(['at://alice2.test'])
117
102
  expect(handleToDid).toEqual(alice)
118
103
  expect(accountFromPds?.handle).toEqual('alice2.test')
@@ -128,13 +113,10 @@ describe('entryway', () => {
128
113
  'com.atproto.identity.updateHandle',
129
114
  ),
130
115
  )
131
- const doc = await entryway.ctx.idResolver.did.resolve(alice)
132
- const handleToDid =
133
- await entryway.ctx.idResolver.handle.resolve('alice3.test')
116
+ const doc = await entryway.idResolver.did.resolve(alice)
117
+ const handleToDid = await entryway.idResolver.handle.resolve('alice3.test')
134
118
  const accountFromPds = await pds.ctx.accountManager.getAccount(alice)
135
- const accountFromEntryway = await entryway.ctx.services
136
- .account(entryway.ctx.db)
137
- .getAccount(alice)
119
+ const accountFromEntryway = entryway.getAccount(alice)
138
120
  expect(doc?.alsoKnownAs).toEqual(['at://alice3.test'])
139
121
  expect(handleToDid).toEqual(alice)
140
122
  expect(accountFromPds?.handle).toEqual('alice3.test')
@@ -148,7 +130,7 @@ describe('entryway', () => {
148
130
  const rotationKey = await Secp256k1Keypair.create()
149
131
  const plcCreate = await plcLib.createOp({
150
132
  signingKey,
151
- rotationKeys: [rotationKey.did(), entryway.ctx.plcRotationKey.did()],
133
+ rotationKeys: [rotationKey.did(), entryway.plcRotationKey.did()],
152
134
  handle: 'weirdalice.test',
153
135
  pds: pds.ctx.cfg.service.publicUrl,
154
136
  signer: rotationKey,
@@ -161,67 +143,3 @@ describe('entryway', () => {
161
143
  await expect(tryCreateAccount).rejects.toThrow('invalid plc operation')
162
144
  })
163
145
  })
164
-
165
- const createEntryway = async (
166
- config: pdsEntryway.ServerEnvironment & {
167
- adminPassword: string
168
- jwtSigningKeyK256PrivateKeyHex: string
169
- plcRotationKeyK256PrivateKeyHex: string
170
- },
171
- ) => {
172
- const signingKey = await Secp256k1Keypair.create({ exportable: true })
173
- const recoveryKey = await Secp256k1Keypair.create({ exportable: true })
174
- const env: pdsEntryway.ServerEnvironment = {
175
- isEntryway: true,
176
- recoveryDidKey: recoveryKey.did(),
177
- serviceHandleDomains: ['.test'],
178
- dbPostgresUrl: process.env.DB_POSTGRES_URL,
179
- blobstoreDiskLocation: path.join(os.tmpdir(), randomStr(8, 'base32')),
180
- bskyAppViewUrl: 'https://appview.invalid',
181
- bskyAppViewDid: 'did:example:invalid',
182
- bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s',
183
- jwtSecret: randomStr(8, 'base32'),
184
- repoSigningKeyK256PrivateKeyHex: await getPrivateHex(signingKey),
185
- modServiceUrl: 'https://mod.invalid',
186
- modServiceDid: 'did:example:invalid',
187
- ...config,
188
- }
189
- const cfg = pdsEntryway.envToCfg(env)
190
- const secrets = pdsEntryway.envToSecrets(env)
191
- const server = await pdsEntryway.PDS.create(cfg, secrets)
192
- await server.ctx.db.migrateToLatestOrThrow()
193
- await server.start()
194
- // patch entryway access token verification to handle internal service auth pds -> entryway
195
- const origValidateAccessToken =
196
- server.ctx.authVerifier.validateAccessToken.bind(server.ctx.authVerifier)
197
- server.ctx.authVerifier.validateAccessToken = async (req, scopes) => {
198
- const jwt = req.headers.authorization?.replace('Bearer ', '') ?? ''
199
- const claims = decodeJwt(jwt)
200
- if (claims.aud === 'did:example:entryway') {
201
- assert(claims.lxm === parseReqNsid(req), 'bad lxm claim in service auth')
202
- assert(claims.aud, 'missing aud claim in service auth')
203
- assert(claims.iss, 'missing iss claim in service auth')
204
- return {
205
- artifacts: jwt,
206
- credentials: {
207
- type: 'access',
208
- scope: 'com.atproto.access' as any,
209
- audience: claims.aud,
210
- did: claims.iss,
211
- },
212
- }
213
- }
214
- return origValidateAccessToken(req, scopes)
215
- }
216
- // @TODO temp hack because entryway teardown calls signupActivator.run() by mistake
217
- server.ctx.signupActivator.run = server.ctx.signupActivator.destroy
218
- return server
219
- }
220
-
221
- const getPublicHex = (key: Secp256k1Keypair) => {
222
- return key.publicKeyStr('hex')
223
- }
224
-
225
- const getPrivateHex = async (key: Secp256k1Keypair) => {
226
- return ui8.toString(await key.export(), 'hex')
227
- }