@shware/security 4.0.0 → 5.0.0

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 (82) hide show
  1. package/README.md +110 -99
  2. package/dist/auth/index.cjs +44 -36
  3. package/dist/auth/index.cjs.map +1 -1
  4. package/dist/auth/index.d.cts +6 -6
  5. package/dist/auth/index.d.cts.map +1 -1
  6. package/dist/auth/index.d.mts +6 -6
  7. package/dist/auth/index.d.mts.map +1 -1
  8. package/dist/auth/index.mjs +44 -36
  9. package/dist/auth/index.mjs.map +1 -1
  10. package/dist/auth/types.d.cts +12 -7
  11. package/dist/auth/types.d.cts.map +1 -1
  12. package/dist/auth/types.d.mts +12 -7
  13. package/dist/auth/types.d.mts.map +1 -1
  14. package/dist/index.cjs +4 -1
  15. package/dist/index.d.cts +3 -2
  16. package/dist/index.d.mts +3 -2
  17. package/dist/index.mjs +3 -2
  18. package/dist/oauth2/client.cjs +113 -49
  19. package/dist/oauth2/client.cjs.map +1 -1
  20. package/dist/oauth2/client.d.cts +54 -14
  21. package/dist/oauth2/client.d.cts.map +1 -1
  22. package/dist/oauth2/client.d.mts +54 -14
  23. package/dist/oauth2/client.d.mts.map +1 -1
  24. package/dist/oauth2/client.mjs +113 -49
  25. package/dist/oauth2/client.mjs.map +1 -1
  26. package/dist/oauth2/name.cjs +107 -0
  27. package/dist/oauth2/name.cjs.map +1 -0
  28. package/dist/oauth2/name.d.cts +49 -0
  29. package/dist/oauth2/name.d.cts.map +1 -0
  30. package/dist/oauth2/name.d.mts +49 -0
  31. package/dist/oauth2/name.d.mts.map +1 -0
  32. package/dist/oauth2/name.mjs +101 -0
  33. package/dist/oauth2/name.mjs.map +1 -0
  34. package/dist/oauth2/provider/apple-client-secret.cjs +3 -3
  35. package/dist/oauth2/provider/apple-client-secret.cjs.map +1 -1
  36. package/dist/oauth2/provider/apple-client-secret.d.cts +3 -3
  37. package/dist/oauth2/provider/apple-client-secret.d.mts +3 -3
  38. package/dist/oauth2/provider/apple-client-secret.mjs +3 -3
  39. package/dist/oauth2/provider/apple-client-secret.mjs.map +1 -1
  40. package/dist/oauth2/provider/apple.cjs +38 -10
  41. package/dist/oauth2/provider/apple.cjs.map +1 -1
  42. package/dist/oauth2/provider/apple.d.cts +19 -14
  43. package/dist/oauth2/provider/apple.d.cts.map +1 -1
  44. package/dist/oauth2/provider/apple.d.mts +19 -14
  45. package/dist/oauth2/provider/apple.d.mts.map +1 -1
  46. package/dist/oauth2/provider/apple.mjs +38 -10
  47. package/dist/oauth2/provider/apple.mjs.map +1 -1
  48. package/dist/oauth2/provider/common.cjs +13 -9
  49. package/dist/oauth2/provider/common.cjs.map +1 -1
  50. package/dist/oauth2/provider/common.d.cts +13 -2
  51. package/dist/oauth2/provider/common.d.cts.map +1 -1
  52. package/dist/oauth2/provider/common.d.mts +13 -2
  53. package/dist/oauth2/provider/common.d.mts.map +1 -1
  54. package/dist/oauth2/provider/common.mjs +13 -9
  55. package/dist/oauth2/provider/common.mjs.map +1 -1
  56. package/dist/oauth2/provider/facebook.cjs +59 -35
  57. package/dist/oauth2/provider/facebook.cjs.map +1 -1
  58. package/dist/oauth2/provider/facebook.d.cts.map +1 -1
  59. package/dist/oauth2/provider/facebook.d.mts.map +1 -1
  60. package/dist/oauth2/provider/facebook.mjs +59 -35
  61. package/dist/oauth2/provider/facebook.mjs.map +1 -1
  62. package/dist/oauth2/provider/google.cjs +31 -32
  63. package/dist/oauth2/provider/google.cjs.map +1 -1
  64. package/dist/oauth2/provider/google.d.cts.map +1 -1
  65. package/dist/oauth2/provider/google.d.mts.map +1 -1
  66. package/dist/oauth2/provider/google.mjs +31 -32
  67. package/dist/oauth2/provider/google.mjs.map +1 -1
  68. package/dist/oauth2/provider/index.cjs +1 -1
  69. package/dist/oauth2/provider/index.mjs +1 -1
  70. package/dist/oauth2/provider/wechat.cjs +1 -0
  71. package/dist/oauth2/provider/wechat.cjs.map +1 -1
  72. package/dist/oauth2/provider/wechat.d.cts.map +1 -1
  73. package/dist/oauth2/provider/wechat.d.mts.map +1 -1
  74. package/dist/oauth2/provider/wechat.mjs +1 -0
  75. package/dist/oauth2/provider/wechat.mjs.map +1 -1
  76. package/dist/oauth2/types.cjs.map +1 -1
  77. package/dist/oauth2/types.d.cts +100 -33
  78. package/dist/oauth2/types.d.cts.map +1 -1
  79. package/dist/oauth2/types.d.mts +100 -33
  80. package/dist/oauth2/types.d.mts.map +1 -1
  81. package/dist/oauth2/types.mjs.map +1 -1
  82. package/package.json +1 -1
@@ -0,0 +1,49 @@
1
+ import { PersonNameComponents, StandardClaims } from "./types.cjs";
2
+ //#region src/oauth2/name.d.ts
3
+ /**
4
+ * Upper bound, in characters, on each name part taken from the client. Names that reach us outside
5
+ * a signed token (Apple's form_post `user`, a native login body) are plain client input, so they
6
+ * are capped before a host stores them; 64 leaves room for any real given or family name.
7
+ */
8
+ export declare const MAX_NAME_PART_LENGTH = 64;
9
+ /**
10
+ * Composes a display `name` from its parts, for providers that hand out the parts only. Chinese,
11
+ * Japanese and Korean names are written family name first without a space (`王小明`), every other
12
+ * name given name first (`John Appleseed`), which is also the safe choice for mixed scripts. With
13
+ * one part only, that part is the name.
14
+ */
15
+ export declare function composeName({ given_name, family_name }: Pick<StandardClaims, 'given_name' | 'family_name'>): string | undefined;
16
+ /**
17
+ * One untrusted name part: a non-blank string, trimmed and capped at `MAX_NAME_PART_LENGTH`
18
+ * characters (code points, so a cut never splits one). Anything else is dropped rather than refused,
19
+ * because the name is a nicety and must never cost the user their login.
20
+ */
21
+ export declare function cleanNamePart(value: unknown): string | undefined;
22
+ /** Name parts as Apple's form_post `user` carries them, after cleaning. */
23
+ export interface AppleUser {
24
+ name?: {
25
+ firstName?: string;
26
+ lastName?: string;
27
+ };
28
+ email?: string;
29
+ }
30
+ /**
31
+ * Parses Apple's form_post `user` (JSON, first authorization only). Malformed JSON or a wrong shape
32
+ * yields `undefined`, never an error. Only known fields are kept, each cleaned, so what a host
33
+ * persists stays small.
34
+ */
35
+ export declare function parseAppleUser(raw: string | undefined): AppleUser | undefined;
36
+ /**
37
+ * Cleans the `fullName` of a native login body (iOS `PersonNameComponents`): known keys only,
38
+ * each a cleaned string; `undefined` when nothing usable is left.
39
+ */
40
+ export declare function cleanPersonName(value: unknown): PersonNameComponents | undefined;
41
+ type NameClaims = Pick<StandardClaims, 'name' | 'given_name' | 'family_name' | 'middle_name' | 'nickname'>;
42
+ /**
43
+ * The standard name claims from cleaned parts, with `name` composed from given and family name.
44
+ * Undefined parts are left out, so spreading the result never overwrites a claim with `undefined`.
45
+ */
46
+ export declare function nameClaims(parts: Omit<NameClaims, 'name'>): NameClaims;
47
+ //#endregion
48
+ export {};
49
+ //# sourceMappingURL=name.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"name.d.cts","names":[],"sources":["../../src/oauth2/name.ts"],"mappings":";;;;;;;qBAOa;;;;;;;wBAaG,cACd,YACA,eACC,KAAK;;;;;;wBAcQ,cAAc;;iBAQb;EACf;IAAS;IAAoB;;EAC7B;;;;;;;wBAQc,eAAe,0BAA0B;;;;;wBAkCzC,gBAAgB,iBAAiB;KAU5C,aAAa,KAChB;;;;;wBAQc,WAAW,OAAO,KAAK,sBAAsB"}
@@ -0,0 +1,49 @@
1
+ import { PersonNameComponents, StandardClaims } from "./types.mjs";
2
+ //#region src/oauth2/name.d.ts
3
+ /**
4
+ * Upper bound, in characters, on each name part taken from the client. Names that reach us outside
5
+ * a signed token (Apple's form_post `user`, a native login body) are plain client input, so they
6
+ * are capped before a host stores them; 64 leaves room for any real given or family name.
7
+ */
8
+ export declare const MAX_NAME_PART_LENGTH = 64;
9
+ /**
10
+ * Composes a display `name` from its parts, for providers that hand out the parts only. Chinese,
11
+ * Japanese and Korean names are written family name first without a space (`王小明`), every other
12
+ * name given name first (`John Appleseed`), which is also the safe choice for mixed scripts. With
13
+ * one part only, that part is the name.
14
+ */
15
+ export declare function composeName({ given_name, family_name }: Pick<StandardClaims, 'given_name' | 'family_name'>): string | undefined;
16
+ /**
17
+ * One untrusted name part: a non-blank string, trimmed and capped at `MAX_NAME_PART_LENGTH`
18
+ * characters (code points, so a cut never splits one). Anything else is dropped rather than refused,
19
+ * because the name is a nicety and must never cost the user their login.
20
+ */
21
+ export declare function cleanNamePart(value: unknown): string | undefined;
22
+ /** Name parts as Apple's form_post `user` carries them, after cleaning. */
23
+ export interface AppleUser {
24
+ name?: {
25
+ firstName?: string;
26
+ lastName?: string;
27
+ };
28
+ email?: string;
29
+ }
30
+ /**
31
+ * Parses Apple's form_post `user` (JSON, first authorization only). Malformed JSON or a wrong shape
32
+ * yields `undefined`, never an error. Only known fields are kept, each cleaned, so what a host
33
+ * persists stays small.
34
+ */
35
+ export declare function parseAppleUser(raw: string | undefined): AppleUser | undefined;
36
+ /**
37
+ * Cleans the `fullName` of a native login body (iOS `PersonNameComponents`): known keys only,
38
+ * each a cleaned string; `undefined` when nothing usable is left.
39
+ */
40
+ export declare function cleanPersonName(value: unknown): PersonNameComponents | undefined;
41
+ type NameClaims = Pick<StandardClaims, 'name' | 'given_name' | 'family_name' | 'middle_name' | 'nickname'>;
42
+ /**
43
+ * The standard name claims from cleaned parts, with `name` composed from given and family name.
44
+ * Undefined parts are left out, so spreading the result never overwrites a claim with `undefined`.
45
+ */
46
+ export declare function nameClaims(parts: Omit<NameClaims, 'name'>): NameClaims;
47
+ //#endregion
48
+ export {};
49
+ //# sourceMappingURL=name.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"name.d.mts","names":[],"sources":["../../src/oauth2/name.ts"],"mappings":";;;;;;;qBAOa;;;;;;;wBAaG,cACd,YACA,eACC,KAAK;;;;;;wBAcQ,cAAc;;iBAQb;EACf;IAAS;IAAoB;;EAC7B;;;;;;;wBAQc,eAAe,0BAA0B;;;;;wBAkCzC,gBAAgB,iBAAiB;KAU5C,aAAa,KAChB;;;;;wBAQc,WAAW,OAAO,KAAK,sBAAsB"}
@@ -0,0 +1,101 @@
1
+ //#region src/oauth2/name.ts
2
+ /**
3
+ * Upper bound, in characters, on each name part taken from the client. Names that reach us outside
4
+ * a signed token (Apple's form_post `user`, a native login body) are plain client input, so they
5
+ * are capped before a host stores them; 64 leaves room for any real given or family name.
6
+ */
7
+ const MAX_NAME_PART_LENGTH = 64;
8
+ /** Apple's form_post `user` is a few dozen bytes; anything far larger is not Apple's. */
9
+ const MAX_APPLE_USER_LENGTH = 1024;
10
+ const CJK = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+$/u;
11
+ /**
12
+ * Composes a display `name` from its parts, for providers that hand out the parts only. Chinese,
13
+ * Japanese and Korean names are written family name first without a space (`王小明`), every other
14
+ * name given name first (`John Appleseed`), which is also the safe choice for mixed scripts. With
15
+ * one part only, that part is the name.
16
+ */
17
+ function composeName({ given_name, family_name }) {
18
+ const given = given_name?.trim();
19
+ const family = family_name?.trim();
20
+ if (given && family) return CJK.test(given) && CJK.test(family) ? `${family}${given}` : `${given} ${family}`;
21
+ return given || family || void 0;
22
+ }
23
+ /**
24
+ * One untrusted name part: a non-blank string, trimmed and capped at `MAX_NAME_PART_LENGTH`
25
+ * characters (code points, so a cut never splits one). Anything else is dropped rather than refused,
26
+ * because the name is a nicety and must never cost the user their login.
27
+ */
28
+ function cleanNamePart(value) {
29
+ if (typeof value !== "string") return void 0;
30
+ const trimmed = value.trim();
31
+ if (!trimmed) return void 0;
32
+ return Array.from(trimmed).slice(0, 64).join("");
33
+ }
34
+ /**
35
+ * Parses Apple's form_post `user` (JSON, first authorization only). Malformed JSON or a wrong shape
36
+ * yields `undefined`, never an error. Only known fields are kept, each cleaned, so what a host
37
+ * persists stays small.
38
+ */
39
+ function parseAppleUser(raw) {
40
+ if (!raw || raw.length > MAX_APPLE_USER_LENGTH) return void 0;
41
+ let json;
42
+ try {
43
+ json = JSON.parse(raw);
44
+ } catch {
45
+ return;
46
+ }
47
+ if (!isObject(json)) return void 0;
48
+ const user = {};
49
+ if (isObject(json.name)) {
50
+ const firstName = cleanNamePart(json.name.firstName);
51
+ const lastName = cleanNamePart(json.name.lastName);
52
+ if (firstName || lastName) user.name = withoutUndefined({
53
+ firstName,
54
+ lastName
55
+ });
56
+ }
57
+ const email = typeof json.email === "string" ? json.email.trim().slice(0, 320) : void 0;
58
+ if (email) user.email = email;
59
+ return user.name || user.email ? user : void 0;
60
+ }
61
+ const PERSON_NAME_KEYS = [
62
+ "givenName",
63
+ "familyName",
64
+ "middleName",
65
+ "namePrefix",
66
+ "nameSuffix",
67
+ "nickname"
68
+ ];
69
+ /**
70
+ * Cleans the `fullName` of a native login body (iOS `PersonNameComponents`): known keys only,
71
+ * each a cleaned string; `undefined` when nothing usable is left.
72
+ */
73
+ function cleanPersonName(value) {
74
+ if (!isObject(value)) return void 0;
75
+ const name = {};
76
+ for (const key of PERSON_NAME_KEYS) {
77
+ const part = cleanNamePart(value[key]);
78
+ if (part) name[key] = part;
79
+ }
80
+ return Object.keys(name).length > 0 ? name : void 0;
81
+ }
82
+ /**
83
+ * The standard name claims from cleaned parts, with `name` composed from given and family name.
84
+ * Undefined parts are left out, so spreading the result never overwrites a claim with `undefined`.
85
+ */
86
+ function nameClaims(parts) {
87
+ return withoutUndefined({
88
+ ...parts,
89
+ name: composeName(parts)
90
+ });
91
+ }
92
+ function isObject(value) {
93
+ return typeof value === "object" && value !== null && !Array.isArray(value);
94
+ }
95
+ function withoutUndefined(value) {
96
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
97
+ }
98
+ //#endregion
99
+ export { MAX_NAME_PART_LENGTH, cleanNamePart, cleanPersonName, composeName, nameClaims, parseAppleUser };
100
+
101
+ //# sourceMappingURL=name.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"name.mjs","names":[],"sources":["../../src/oauth2/name.ts"],"sourcesContent":["import type { PersonNameComponents, StandardClaims } from './types';\n\n/**\n * Upper bound, in characters, on each name part taken from the client. Names that reach us outside\n * a signed token (Apple's form_post `user`, a native login body) are plain client input, so they\n * are capped before a host stores them; 64 leaves room for any real given or family name.\n */\nexport const MAX_NAME_PART_LENGTH = 64;\n\n/** Apple's form_post `user` is a few dozen bytes; anything far larger is not Apple's. */\nconst MAX_APPLE_USER_LENGTH = 1024;\n\nconst CJK = /^[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]+$/u;\n\n/**\n * Composes a display `name` from its parts, for providers that hand out the parts only. Chinese,\n * Japanese and Korean names are written family name first without a space (`王小明`), every other\n * name given name first (`John Appleseed`), which is also the safe choice for mixed scripts. With\n * one part only, that part is the name.\n */\nexport function composeName({\n given_name,\n family_name,\n}: Pick<StandardClaims, 'given_name' | 'family_name'>): string | undefined {\n const given = given_name?.trim();\n const family = family_name?.trim();\n if (given && family) {\n return CJK.test(given) && CJK.test(family) ? `${family}${given}` : `${given} ${family}`;\n }\n return given || family || undefined;\n}\n\n/**\n * One untrusted name part: a non-blank string, trimmed and capped at `MAX_NAME_PART_LENGTH`\n * characters (code points, so a cut never splits one). Anything else is dropped rather than refused,\n * because the name is a nicety and must never cost the user their login.\n */\nexport function cleanNamePart(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n return Array.from(trimmed).slice(0, MAX_NAME_PART_LENGTH).join('');\n}\n\n/** Name parts as Apple's form_post `user` carries them, after cleaning. */\nexport interface AppleUser {\n name?: { firstName?: string; lastName?: string };\n email?: string;\n}\n\n/**\n * Parses Apple's form_post `user` (JSON, first authorization only). Malformed JSON or a wrong shape\n * yields `undefined`, never an error. Only known fields are kept, each cleaned, so what a host\n * persists stays small.\n */\nexport function parseAppleUser(raw: string | undefined): AppleUser | undefined {\n if (!raw || raw.length > MAX_APPLE_USER_LENGTH) return undefined;\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isObject(json)) return undefined;\n const user: AppleUser = {};\n if (isObject(json.name)) {\n const firstName = cleanNamePart(json.name.firstName);\n const lastName = cleanNamePart(json.name.lastName);\n if (firstName || lastName) user.name = withoutUndefined({ firstName, lastName });\n }\n // Kept as Apple sent it for the record only: the id token's email is signed, this one is not.\n const email = typeof json.email === 'string' ? json.email.trim().slice(0, 320) : undefined;\n if (email) user.email = email;\n return user.name || user.email ? user : undefined;\n}\n\nconst PERSON_NAME_KEYS = [\n 'givenName',\n 'familyName',\n 'middleName',\n 'namePrefix',\n 'nameSuffix',\n 'nickname',\n] as const satisfies (keyof PersonNameComponents)[];\n\n/**\n * Cleans the `fullName` of a native login body (iOS `PersonNameComponents`): known keys only,\n * each a cleaned string; `undefined` when nothing usable is left.\n */\nexport function cleanPersonName(value: unknown): PersonNameComponents | undefined {\n if (!isObject(value)) return undefined;\n const name: PersonNameComponents = {};\n for (const key of PERSON_NAME_KEYS) {\n const part = cleanNamePart(value[key]);\n if (part) name[key] = part;\n }\n return Object.keys(name).length > 0 ? name : undefined;\n}\n\ntype NameClaims = Pick<\n StandardClaims,\n 'name' | 'given_name' | 'family_name' | 'middle_name' | 'nickname'\n>;\n\n/**\n * The standard name claims from cleaned parts, with `name` composed from given and family name.\n * Undefined parts are left out, so spreading the result never overwrites a claim with `undefined`.\n */\nexport function nameClaims(parts: Omit<NameClaims, 'name'>): NameClaims {\n return withoutUndefined({ ...parts, name: composeName(parts) });\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction withoutUndefined<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n"],"mappings":";;;;;;AAOA,MAAa,uBAAuB;;AAGpC,MAAM,wBAAwB;AAE9B,MAAM,MAAM;;;;;;;AAQZ,SAAgB,YAAY,EAC1B,YACA,eACyE;CACzE,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,SAAS,aAAa,KAAK;CACjC,IAAI,SAAS,QACX,OAAO,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,SAAS,UAAU,GAAG,MAAM,GAAG;CAEjF,OAAO,SAAS,UAAU,KAAA;AAC5B;;;;;;AAOA,SAAgB,cAAc,OAAoC;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,OAAO,MAAM,KAAK,OAAO,CAAC,CAAC,MAAM,GAAA,EAAuB,CAAC,CAAC,KAAK,EAAE;AACnE;;;;;;AAaA,SAAgB,eAAe,KAAgD;CAC7E,IAAI,CAAC,OAAO,IAAI,SAAS,uBAAuB,OAAO,KAAA;CACvD,IAAI;CACJ,IAAI;EACF,OAAO,KAAK,MAAM,GAAG;CACvB,QAAQ;EACN;CACF;CACA,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO,KAAA;CAC5B,MAAM,OAAkB,CAAC;CACzB,IAAI,SAAS,KAAK,IAAI,GAAG;EACvB,MAAM,YAAY,cAAc,KAAK,KAAK,SAAS;EACnD,MAAM,WAAW,cAAc,KAAK,KAAK,QAAQ;EACjD,IAAI,aAAa,UAAU,KAAK,OAAO,iBAAiB;GAAE;GAAW;EAAS,CAAC;CACjF;CAEA,MAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,KAAK,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,IAAI,KAAA;CACjF,IAAI,OAAO,KAAK,QAAQ;CACxB,OAAO,KAAK,QAAQ,KAAK,QAAQ,OAAO,KAAA;AAC1C;AAEA,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,SAAgB,gBAAgB,OAAkD;CAChF,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO,KAAA;CAC7B,MAAM,OAA6B,CAAC;CACpC,KAAK,MAAM,OAAO,kBAAkB;EAClC,MAAM,OAAO,cAAc,MAAM,IAAI;EACrC,IAAI,MAAM,KAAK,OAAO;CACxB;CACA,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,KAAA;AAC/C;;;;;AAWA,SAAgB,WAAW,OAA6C;CACtE,OAAO,iBAAiB;EAAE,GAAG;EAAO,MAAM,YAAY,KAAK;CAAE,CAAC;AAChE;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,iBAAmC,OAAa;CACvD,OAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,QAAQ,GAAG,OAAO,MAAM,KAAA,CAAS,CAAC;AACpF"}
@@ -8,9 +8,9 @@ let jose = require("jose");
8
8
  * differ only in `sub`.
9
9
  *
10
10
  * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
11
- * seconds before expiry, so the secret can be minted by the server at runtime (an
12
- * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
13
- * rotated, as a static secret.
11
+ * seconds before expiry. Pass it as the `clientSecret` of every Apple client (web and native): it
12
+ * is called with each client's own id on every use, so the server mints the secrets at runtime
13
+ * instead of generating, storing and rotating static ones by hand.
14
14
  */
15
15
  function createAppleClientSecret(options) {
16
16
  const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;
@@ -1 +1 @@
1
- {"version":3,"file":"apple-client-secret.cjs","names":["importPKCS8","SignJWT"],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"sourcesContent":["import { SignJWT, importPKCS8 } from 'jose';\n\nexport interface AppleClientSecretOptions {\n /** Apple Developer Team ID (Membership details). */\n teamId: string;\n /** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */\n keyId: string;\n /** Contents of that key's `.p8` file: a PKCS#8 PEM. */\n privateKey: string;\n /** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */\n expiresIn?: number;\n /** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */\n renewBefore?: number;\n}\n\n/**\n * Sign in with Apple has no static client secret: the secret is a JWT the server signs with its\n * Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and\n * a team signs one per client id — the app's bundle id and the web Services ID share the key and\n * differ only in `sub`.\n *\n * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`\n * seconds before expiry, so the secret can be minted by the server at runtime (an\n * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and\n * rotated, as a static secret.\n */\nexport function createAppleClientSecret(options: AppleClientSecretOptions) {\n const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;\n if (renewBefore >= expiresIn) {\n throw new Error('renewBefore must be shorter than expiresIn');\n }\n\n // The import is cached as a promise so concurrent first calls share one; a failed import is\n // dropped so the next call retries instead of failing forever.\n let key: Promise<CryptoKey> | undefined;\n const cache = new Map<string, { secret: string; exp: number }>();\n\n function getKey() {\n key ??= importPKCS8(privateKey, 'ES256').catch((error: unknown) => {\n key = undefined;\n throw error;\n });\n return key;\n }\n\n return async function appleClientSecret(clientId: string): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const cached = cache.get(clientId);\n if (cached && cached.exp - now > renewBefore) return cached.secret;\n\n const exp = now + expiresIn;\n const secret = await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: keyId })\n .setIssuer(teamId)\n .setSubject(clientId)\n .setAudience('https://appleid.apple.com')\n .setIssuedAt(now)\n .setExpirationTime(exp)\n .sign(await getKey());\n cache.set(clientId, { secret, exp });\n return secret;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,QAAQ,OAAO,YAAY,YAAY,MAAM,cAAc,QAAQ;CAC3E,IAAI,eAAe,WACjB,MAAM,IAAI,MAAM,4CAA4C;CAK9D,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA6C;CAE/D,SAAS,SAAS;EAChB,SAAA,GAAQA,KAAAA,YAAAA,CAAY,YAAY,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjE,MAAM,KAAA;GACN,MAAM;EACR,CAAC;EACD,OAAO;CACT;CAEA,OAAO,eAAe,kBAAkB,UAAmC;EACzE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EACxC,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,UAAU,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO;EAE5D,MAAM,MAAM,MAAM;EAClB,MAAM,SAAS,MAAM,IAAIC,KAAAA,QAAQ,CAAC,CAAC,CAAC,CACjC,mBAAmB;GAAE,KAAK;GAAS,KAAK;EAAM,CAAC,CAAC,CAChD,UAAU,MAAM,CAAC,CACjB,WAAW,QAAQ,CAAC,CACpB,YAAY,2BAA2B,CAAC,CACxC,YAAY,GAAG,CAAC,CAChB,kBAAkB,GAAG,CAAC,CACtB,KAAK,MAAM,OAAO,CAAC;EACtB,MAAM,IAAI,UAAU;GAAE;GAAQ;EAAI,CAAC;EACnC,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"apple-client-secret.cjs","names":["importPKCS8","SignJWT"],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"sourcesContent":["import { SignJWT, importPKCS8 } from 'jose';\n\nexport interface AppleClientSecretOptions {\n /** Apple Developer Team ID (Membership details). */\n teamId: string;\n /** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */\n keyId: string;\n /** Contents of that key's `.p8` file: a PKCS#8 PEM. */\n privateKey: string;\n /** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */\n expiresIn?: number;\n /** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */\n renewBefore?: number;\n}\n\n/**\n * Sign in with Apple has no static client secret: the secret is a JWT the server signs with its\n * Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and\n * a team signs one per client id — the app's bundle id and the web Services ID share the key and\n * differ only in `sub`.\n *\n * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`\n * seconds before expiry. Pass it as the `clientSecret` of every Apple client (web and native): it\n * is called with each client's own id on every use, so the server mints the secrets at runtime\n * instead of generating, storing and rotating static ones by hand.\n */\nexport function createAppleClientSecret(options: AppleClientSecretOptions) {\n const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;\n if (renewBefore >= expiresIn) {\n throw new Error('renewBefore must be shorter than expiresIn');\n }\n\n // The import is cached as a promise so concurrent first calls share one; a failed import is\n // dropped so the next call retries instead of failing forever.\n let key: Promise<CryptoKey> | undefined;\n const cache = new Map<string, { secret: string; exp: number }>();\n\n function getKey() {\n key ??= importPKCS8(privateKey, 'ES256').catch((error: unknown) => {\n key = undefined;\n throw error;\n });\n return key;\n }\n\n return async function appleClientSecret(clientId: string): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const cached = cache.get(clientId);\n if (cached && cached.exp - now > renewBefore) return cached.secret;\n\n const exp = now + expiresIn;\n const secret = await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: keyId })\n .setIssuer(teamId)\n .setSubject(clientId)\n .setAudience('https://appleid.apple.com')\n .setIssuedAt(now)\n .setExpirationTime(exp)\n .sign(await getKey());\n cache.set(clientId, { secret, exp });\n return secret;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,QAAQ,OAAO,YAAY,YAAY,MAAM,cAAc,QAAQ;CAC3E,IAAI,eAAe,WACjB,MAAM,IAAI,MAAM,4CAA4C;CAK9D,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA6C;CAE/D,SAAS,SAAS;EAChB,SAAA,GAAQA,KAAAA,YAAAA,CAAY,YAAY,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjE,MAAM,KAAA;GACN,MAAM;EACR,CAAC;EACD,OAAO;CACT;CAEA,OAAO,eAAe,kBAAkB,UAAmC;EACzE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EACxC,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,UAAU,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO;EAE5D,MAAM,MAAM,MAAM;EAClB,MAAM,SAAS,MAAM,IAAIC,KAAAA,QAAQ,CAAC,CAAC,CAAC,CACjC,mBAAmB;GAAE,KAAK;GAAS,KAAK;EAAM,CAAC,CAAC,CAChD,UAAU,MAAM,CAAC,CACjB,WAAW,QAAQ,CAAC,CACpB,YAAY,2BAA2B,CAAC,CACxC,YAAY,GAAG,CAAC,CAChB,kBAAkB,GAAG,CAAC,CACtB,KAAK,MAAM,OAAO,CAAC;EACtB,MAAM,IAAI,UAAU;GAAE;GAAQ;EAAI,CAAC;EACnC,OAAO;CACT;AACF"}
@@ -18,9 +18,9 @@ export interface AppleClientSecretOptions {
18
18
  * differ only in `sub`.
19
19
  *
20
20
  * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
21
- * seconds before expiry, so the secret can be minted by the server at runtime (an
22
- * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
23
- * rotated, as a static secret.
21
+ * seconds before expiry. Pass it as the `clientSecret` of every Apple client (web and native): it
22
+ * is called with each client's own id on every use, so the server mints the secrets at runtime
23
+ * instead of generating, storing and rotating static ones by hand.
24
24
  */
25
25
  export declare function createAppleClientSecret(options: AppleClientSecretOptions): (clientId: string) => Promise<string>;
26
26
  //#endregion
@@ -18,9 +18,9 @@ export interface AppleClientSecretOptions {
18
18
  * differ only in `sub`.
19
19
  *
20
20
  * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
21
- * seconds before expiry, so the secret can be minted by the server at runtime (an
22
- * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
23
- * rotated, as a static secret.
21
+ * seconds before expiry. Pass it as the `clientSecret` of every Apple client (web and native): it
22
+ * is called with each client's own id on every use, so the server mints the secrets at runtime
23
+ * instead of generating, storing and rotating static ones by hand.
24
24
  */
25
25
  export declare function createAppleClientSecret(options: AppleClientSecretOptions): (clientId: string) => Promise<string>;
26
26
  //#endregion
@@ -7,9 +7,9 @@ import { SignJWT, importPKCS8 } from "jose";
7
7
  * differ only in `sub`.
8
8
  *
9
9
  * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
10
- * seconds before expiry, so the secret can be minted by the server at runtime (an
11
- * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
12
- * rotated, as a static secret.
10
+ * seconds before expiry. Pass it as the `clientSecret` of every Apple client (web and native): it
11
+ * is called with each client's own id on every use, so the server mints the secrets at runtime
12
+ * instead of generating, storing and rotating static ones by hand.
13
13
  */
14
14
  function createAppleClientSecret(options) {
15
15
  const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;
@@ -1 +1 @@
1
- {"version":3,"file":"apple-client-secret.mjs","names":[],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"sourcesContent":["import { SignJWT, importPKCS8 } from 'jose';\n\nexport interface AppleClientSecretOptions {\n /** Apple Developer Team ID (Membership details). */\n teamId: string;\n /** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */\n keyId: string;\n /** Contents of that key's `.p8` file: a PKCS#8 PEM. */\n privateKey: string;\n /** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */\n expiresIn?: number;\n /** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */\n renewBefore?: number;\n}\n\n/**\n * Sign in with Apple has no static client secret: the secret is a JWT the server signs with its\n * Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and\n * a team signs one per client id — the app's bundle id and the web Services ID share the key and\n * differ only in `sub`.\n *\n * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`\n * seconds before expiry, so the secret can be minted by the server at runtime (an\n * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and\n * rotated, as a static secret.\n */\nexport function createAppleClientSecret(options: AppleClientSecretOptions) {\n const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;\n if (renewBefore >= expiresIn) {\n throw new Error('renewBefore must be shorter than expiresIn');\n }\n\n // The import is cached as a promise so concurrent first calls share one; a failed import is\n // dropped so the next call retries instead of failing forever.\n let key: Promise<CryptoKey> | undefined;\n const cache = new Map<string, { secret: string; exp: number }>();\n\n function getKey() {\n key ??= importPKCS8(privateKey, 'ES256').catch((error: unknown) => {\n key = undefined;\n throw error;\n });\n return key;\n }\n\n return async function appleClientSecret(clientId: string): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const cached = cache.get(clientId);\n if (cached && cached.exp - now > renewBefore) return cached.secret;\n\n const exp = now + expiresIn;\n const secret = await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: keyId })\n .setIssuer(teamId)\n .setSubject(clientId)\n .setAudience('https://appleid.apple.com')\n .setIssuedAt(now)\n .setExpirationTime(exp)\n .sign(await getKey());\n cache.set(clientId, { secret, exp });\n return secret;\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,QAAQ,OAAO,YAAY,YAAY,MAAM,cAAc,QAAQ;CAC3E,IAAI,eAAe,WACjB,MAAM,IAAI,MAAM,4CAA4C;CAK9D,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA6C;CAE/D,SAAS,SAAS;EAChB,QAAQ,YAAY,YAAY,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjE,MAAM,KAAA;GACN,MAAM;EACR,CAAC;EACD,OAAO;CACT;CAEA,OAAO,eAAe,kBAAkB,UAAmC;EACzE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EACxC,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,UAAU,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO;EAE5D,MAAM,MAAM,MAAM;EAClB,MAAM,SAAS,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CACjC,mBAAmB;GAAE,KAAK;GAAS,KAAK;EAAM,CAAC,CAAC,CAChD,UAAU,MAAM,CAAC,CACjB,WAAW,QAAQ,CAAC,CACpB,YAAY,2BAA2B,CAAC,CACxC,YAAY,GAAG,CAAC,CAChB,kBAAkB,GAAG,CAAC,CACtB,KAAK,MAAM,OAAO,CAAC;EACtB,MAAM,IAAI,UAAU;GAAE;GAAQ;EAAI,CAAC;EACnC,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"apple-client-secret.mjs","names":[],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"sourcesContent":["import { SignJWT, importPKCS8 } from 'jose';\n\nexport interface AppleClientSecretOptions {\n /** Apple Developer Team ID (Membership details). */\n teamId: string;\n /** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */\n keyId: string;\n /** Contents of that key's `.p8` file: a PKCS#8 PEM. */\n privateKey: string;\n /** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */\n expiresIn?: number;\n /** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */\n renewBefore?: number;\n}\n\n/**\n * Sign in with Apple has no static client secret: the secret is a JWT the server signs with its\n * Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and\n * a team signs one per client id — the app's bundle id and the web Services ID share the key and\n * differ only in `sub`.\n *\n * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`\n * seconds before expiry. Pass it as the `clientSecret` of every Apple client (web and native): it\n * is called with each client's own id on every use, so the server mints the secrets at runtime\n * instead of generating, storing and rotating static ones by hand.\n */\nexport function createAppleClientSecret(options: AppleClientSecretOptions) {\n const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;\n if (renewBefore >= expiresIn) {\n throw new Error('renewBefore must be shorter than expiresIn');\n }\n\n // The import is cached as a promise so concurrent first calls share one; a failed import is\n // dropped so the next call retries instead of failing forever.\n let key: Promise<CryptoKey> | undefined;\n const cache = new Map<string, { secret: string; exp: number }>();\n\n function getKey() {\n key ??= importPKCS8(privateKey, 'ES256').catch((error: unknown) => {\n key = undefined;\n throw error;\n });\n return key;\n }\n\n return async function appleClientSecret(clientId: string): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const cached = cache.get(clientId);\n if (cached && cached.exp - now > renewBefore) return cached.secret;\n\n const exp = now + expiresIn;\n const secret = await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: keyId })\n .setIssuer(teamId)\n .setSubject(clientId)\n .setAudience('https://appleid.apple.com')\n .setIssuedAt(now)\n .setExpirationTime(exp)\n .sign(await getKey());\n cache.set(clientId, { secret, exp });\n return secret;\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,QAAQ,OAAO,YAAY,YAAY,MAAM,cAAc,QAAQ;CAC3E,IAAI,eAAe,WACjB,MAAM,IAAI,MAAM,4CAA4C;CAK9D,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA6C;CAE/D,SAAS,SAAS;EAChB,QAAQ,YAAY,YAAY,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjE,MAAM,KAAA;GACN,MAAM;EACR,CAAC;EACD,OAAO;CACT;CAEA,OAAO,eAAe,kBAAkB,UAAmC;EACzE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EACxC,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,UAAU,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO;EAE5D,MAAM,MAAM,MAAM;EAClB,MAAM,SAAS,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CACjC,mBAAmB;GAAE,KAAK;GAAS,KAAK;EAAM,CAAC,CAAC,CAChD,UAAU,MAAM,CAAC,CACjB,WAAW,QAAQ,CAAC,CACpB,YAAY,2BAA2B,CAAC,CACxC,YAAY,GAAG,CAAC,CAChB,kBAAkB,GAAG,CAAC,CACtB,KAAK,MAAM,OAAO,CAAC;EACtB,MAAM,IAAI,UAAU;GAAE;GAAQ;EAAI,CAAC;EACnC,OAAO;CACT;AACF"}
@@ -1,8 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_oauth2_error = require("../error.cjs");
3
+ const require_oauth2_name = require("../name.cjs");
3
4
  const require_oauth2_provider_common = require("./common.cjs");
4
5
  let _shware_utils = require("@shware/utils");
5
6
  //#region src/oauth2/provider/apple.ts
7
+ /** `iss` of every Apple id token (https://appleid.apple.com/.well-known/openid-configuration). */
8
+ const APPLE_ISSUER = "https://appleid.apple.com";
6
9
  function createAppleProvider() {
7
10
  return {
8
11
  authorizationUri: "https://appleid.apple.com/auth/authorize?response_mode=form_post",
@@ -32,20 +35,28 @@ function createAppleProvider() {
32
35
  }
33
36
  return await response.json();
34
37
  },
35
- async getUserInfo({ id_token }) {
38
+ async getUserInfo({ id_token }, { clientId, response }) {
36
39
  (0, _shware_utils.invariant)(id_token, "id_token is required");
37
40
  (0, _shware_utils.invariant)(this.jwkSetUri, "jwkSetUri is required");
38
- const data = await require_oauth2_provider_common.verifyIdToken(id_token, this.jwkSetUri);
41
+ const token = await require_oauth2_provider_common.verifyIdToken(id_token, {
42
+ jwkSetUri: this.jwkSetUri,
43
+ issuer: APPLE_ISSUER,
44
+ audience: clientId
45
+ });
46
+ const user = require_oauth2_name.parseAppleUser(response?.user);
39
47
  return {
40
- data,
48
+ data: user ? {
49
+ ...token,
50
+ user
51
+ } : token,
41
52
  claims: {
42
- sub: data.sub,
43
- name: data.name,
44
- email: data.email,
53
+ sub: token.sub,
54
+ email: token.email,
45
55
  email_verified: true,
46
- picture: data.picture,
47
- given_name: data.user?.name.firstName,
48
- family_name: data.user?.name.lastName
56
+ ...require_oauth2_name.nameClaims({
57
+ given_name: user?.name?.firstName,
58
+ family_name: user?.name?.lastName
59
+ })
49
60
  }
50
61
  };
51
62
  },
@@ -63,9 +74,26 @@ function createAppleProvider() {
63
74
  throw new require_oauth2_error.OAuth2Error(response.status, error);
64
75
  }
65
76
  const token = await response.json();
77
+ const { data, claims } = await this.getUserInfo(token, { clientId: params.clientId });
78
+ const fullName = require_oauth2_name.cleanPersonName(credentials.fullName);
79
+ const name = require_oauth2_name.nameClaims({
80
+ given_name: fullName?.givenName ?? void 0,
81
+ family_name: fullName?.familyName ?? void 0,
82
+ middle_name: fullName?.middleName ?? void 0,
83
+ nickname: fullName?.nickname ?? void 0
84
+ });
66
85
  return {
67
86
  token,
68
- userInfo: await this.getUserInfo(token)
87
+ userInfo: {
88
+ data: fullName ? {
89
+ ...data,
90
+ fullName
91
+ } : data,
92
+ claims: {
93
+ ...claims,
94
+ ...name
95
+ }
96
+ }
69
97
  };
70
98
  }
71
99
  };
@@ -1 +1 @@
1
- {"version":3,"file":"apple.cjs","names":["createAuthorizationUri","exchangeAuthorizationCode","OAuth2Error","verifyIdToken"],"sources":["../../../src/oauth2/provider/apple.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport type { LoginOAuth2NativeParams, NativeCredential, OAuth2Token, Provider } from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\n// ref: https://account.apple.com/.well-known/openid-configuration\nexport function createAppleProvider(): Provider {\n return {\n // important notice: response_mode=form_post is required for apple\n authorizationUri: 'https://appleid.apple.com/auth/authorize?response_mode=form_post',\n tokenUri: 'https://appleid.apple.com/auth/token',\n jwkSetUri: 'https://appleid.apple.com/auth/keys',\n userNameAttribute: 'sub',\n defaultScope: ['openid', 'name', 'email'],\n createAuthorizationUri({ pkce: _, ...params }) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode({ pkce: _, ...params }) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n return (await response.json()) as AppleToken;\n },\n async getUserInfo({ id_token }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<AppleUserInfo>(id_token, this.jwkSetUri);\n return {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n email: data.email,\n email_verified: true,\n picture: data.picture,\n given_name: data.user?.name.firstName,\n family_name: data.user?.name.lastName,\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { tokenUri } = this;\n const { code } = credentials;\n const response = await exchangeAuthorizationCode({ code, tokenUri, ...params });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n const token = (await response.json()) as AppleToken;\n const userInfo = await this.getUserInfo(token);\n return { token, userInfo };\n },\n };\n}\n\nexport const apple = createAppleProvider();\n\n// https://developer.apple.com/documentation/sign_in_with_apple/authenticating-users-with-sign-in-with-apple\nexport interface AppleUserInfo {\n sub: string;\n email: string;\n email_verified: true | 'true';\n is_private_email: boolean;\n real_user_status: number;\n name: string;\n picture: string;\n user?: {\n name: { firstName: string; lastName: string };\n email: string;\n };\n nonce?: string;\n nonce_supported?: boolean;\n}\n\n// https://developer.apple.com/documentation/devicemanagement/implementing-the-oauth2-authentication-user-enrollment-flow\n// https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens\nexport interface AppleToken extends OAuth2Token {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token: string;\n}\n\nexport interface AppleNativeCredential extends NativeCredential {\n state: string;\n code: string;\n id_token: string;\n // no standard fields\n user: string;\n email?: string;\n}\n\n// https://developer.apple.com/documentation/sign_in_with_apple/errorresponse\nexport interface AppleErrorResponse {\n error:\n | 'invalid_request'\n | 'invalid_client'\n | 'invalid_grant'\n | 'unauthorized_client'\n | 'unsupported_grant_type'\n | 'invalid_scope';\n}\n"],"mappings":";;;;;AAMA,SAAgB,sBAAgC;CAC9C,OAAO;EAEL,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mBAAmB;EACnB,cAAc;GAAC;GAAU;GAAQ;EAAO;EACxC,uBAAuB,EAAE,MAAM,GAAG,GAAG,UAAU;GAC7C,OAAOA,+BAAAA,uBAAuB;IAC5B,GAAG;IACH,OAAO,OAAO,SAAS,KAAK;IAC5B,kBAAkB,KAAK;GACzB,CAAC;EACH;EACA,MAAM,0BAA0B,EAAE,MAAM,GAAG,GAAG,UAAU;GACtD,MAAM,WAAW,MAAMC,+BAAAA,0BAA0B;IAAE,GAAG;IAAQ,UAAU,KAAK;GAAS,CAAC;GACvF,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,OAAQ,MAAM,SAAS,KAAK;EAC9B;EACA,MAAM,YAAY,EAAE,YAAY;GAC9B,CAAA,GAAA,cAAA,UAAA,CAAU,UAAU,sBAAsB;GAC1C,CAAA,GAAA,cAAA,UAAA,CAAU,KAAK,WAAW,uBAAuB;GACjD,MAAM,OAAO,MAAMC,+BAAAA,cAA6B,UAAU,KAAK,SAAS;GACxE,OAAO;IACL;IACA,QAAQ;KACN,KAAK,KAAK;KACV,MAAM,KAAK;KACX,OAAO,KAAK;KACZ,gBAAgB;KAChB,SAAS,KAAK;KACd,YAAY,KAAK,MAAM,KAAK;KAC5B,aAAa,KAAK,MAAM,KAAK;IAC/B;GACF;EACF;EACA,MAAM,kBAAkB,EAAE,MAAM,GAAG,aAAa,GAAG,UAAmC;GACpF,CAAA,GAAA,cAAA,UAAA,CAAU,YAAY,MAAM,kBAAkB;GAC9C,MAAM,EAAE,aAAa;GACrB,MAAM,EAAE,SAAS;GACjB,MAAM,WAAW,MAAMF,+BAAAA,0BAA0B;IAAE;IAAM;IAAU,GAAG;GAAO,CAAC;GAC9E,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,MAAM,QAAS,MAAM,SAAS,KAAK;GAEnC,OAAO;IAAE;IAAO,UAAA,MADO,KAAK,YAAY,KAAK;GACpB;EAC3B;CACF;AACF;AAEA,MAAa,QAAQ,oBAAoB"}
1
+ {"version":3,"file":"apple.cjs","names":["createAuthorizationUri","exchangeAuthorizationCode","OAuth2Error","verifyIdToken","parseAppleUser","nameClaims","cleanPersonName"],"sources":["../../../src/oauth2/provider/apple.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport { type AppleUser, cleanPersonName, nameClaims, parseAppleUser } from '../name';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n PersonNameComponents,\n Provider,\n UserInfo,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\n/** `iss` of every Apple id token (https://appleid.apple.com/.well-known/openid-configuration). */\nconst APPLE_ISSUER = 'https://appleid.apple.com';\n\n// ref: https://account.apple.com/.well-known/openid-configuration\nexport function createAppleProvider(): Provider {\n return {\n // important notice: response_mode=form_post is required for apple\n authorizationUri: 'https://appleid.apple.com/auth/authorize?response_mode=form_post',\n tokenUri: 'https://appleid.apple.com/auth/token',\n jwkSetUri: 'https://appleid.apple.com/auth/keys',\n userNameAttribute: 'sub',\n defaultScope: ['openid', 'name', 'email'],\n createAuthorizationUri({ pkce: _, ...params }) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode({ pkce: _, ...params }) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n return (await response.json()) as AppleToken;\n },\n async getUserInfo({ id_token }, { clientId, response }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n // The audience is the client the code was exchanged as: the bundle id for an app, the\n // Services ID for the web.\n const token = await verifyIdToken<AppleIdToken>(id_token, {\n jwkSetUri: this.jwkSetUri,\n issuer: APPLE_ISSUER,\n audience: clientId,\n });\n // Apple's id token never carries a name; on the first authorization of the web flow the\n // name comes in the form_post `user` instead, unsigned, so only its name parts are used\n // and the signed id token's email always wins over the one beside them.\n const user = parseAppleUser(response?.user);\n const data: AppleUserInfo = user ? { ...token, user } : token;\n return {\n data,\n claims: {\n sub: token.sub,\n email: token.email,\n email_verified: true,\n ...nameClaims({\n given_name: user?.name?.firstName,\n family_name: user?.name?.lastName,\n }),\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { tokenUri } = this;\n const { code } = credentials;\n const response = await exchangeAuthorizationCode({ code, tokenUri, ...params });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n const token = (await response.json()) as AppleToken;\n const { data, claims } = await this.getUserInfo(token, { clientId: params.clientId });\n // The app gets the name from Apple on the first authorization only and passes it along in\n // the login body as the platform's `fullName`.\n const fullName = cleanPersonName(credentials.fullName);\n const name = nameClaims({\n given_name: fullName?.givenName ?? undefined,\n family_name: fullName?.familyName ?? undefined,\n middle_name: fullName?.middleName ?? undefined,\n nickname: fullName?.nickname ?? undefined,\n });\n const userInfo: UserInfo<AppleUserInfo> = {\n data: fullName ? { ...(data as AppleUserInfo), fullName } : (data as AppleUserInfo),\n claims: { ...claims, ...name },\n };\n return { token, userInfo };\n },\n };\n}\n\nexport const apple = createAppleProvider();\n\n// Claims of Apple's id token. It carries no name or picture, not even on the first authorization.\n// https://developer.apple.com/documentation/signinwithapple/authenticating-users-with-sign-in-with-apple\nexport interface AppleIdToken {\n iss: string;\n aud: string;\n exp: number;\n iat: number;\n sub: string;\n email: string;\n email_verified: true | 'true';\n is_private_email: boolean;\n /** Native flow, first authorization only: 0 unsupported, 1 unknown, 2 likely real. */\n real_user_status?: number;\n nonce?: string;\n nonce_supported?: boolean;\n}\n\n/**\n * `userInfo.data` of an Apple login: the id token's claims, plus the unsigned name the client\n * passed on the first authorization, cleaned (see `nameClaims` for the standard claims made of it).\n */\nexport interface AppleUserInfo extends AppleIdToken {\n /** Web flow: Apple's form_post `user`, as Apple names it. */\n user?: AppleUser;\n /** Native flow: the login body's `fullName`, which alone keeps `namePrefix` / `nameSuffix`. */\n fullName?: PersonNameComponents;\n}\n\n// https://developer.apple.com/documentation/devicemanagement/implementing-the-oauth2-authentication-user-enrollment-flow\n// https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens\nexport interface AppleToken extends OAuth2Token {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token: string;\n}\n\nexport interface AppleNativeCredential extends NativeCredential {\n state: string;\n code: string;\n id_token: string;\n}\n\n// https://developer.apple.com/documentation/sign_in_with_apple/errorresponse\nexport interface AppleErrorResponse {\n error:\n | 'invalid_request'\n | 'invalid_client'\n | 'invalid_grant'\n | 'unauthorized_client'\n | 'unsupported_grant_type'\n | 'invalid_scope';\n}\n"],"mappings":";;;;;;;AAcA,MAAM,eAAe;AAGrB,SAAgB,sBAAgC;CAC9C,OAAO;EAEL,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mBAAmB;EACnB,cAAc;GAAC;GAAU;GAAQ;EAAO;EACxC,uBAAuB,EAAE,MAAM,GAAG,GAAG,UAAU;GAC7C,OAAOA,+BAAAA,uBAAuB;IAC5B,GAAG;IACH,OAAO,OAAO,SAAS,KAAK;IAC5B,kBAAkB,KAAK;GACzB,CAAC;EACH;EACA,MAAM,0BAA0B,EAAE,MAAM,GAAG,GAAG,UAAU;GACtD,MAAM,WAAW,MAAMC,+BAAAA,0BAA0B;IAAE,GAAG;IAAQ,UAAU,KAAK;GAAS,CAAC;GACvF,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,OAAQ,MAAM,SAAS,KAAK;EAC9B;EACA,MAAM,YAAY,EAAE,YAAY,EAAE,UAAU,YAAY;GACtD,CAAA,GAAA,cAAA,UAAA,CAAU,UAAU,sBAAsB;GAC1C,CAAA,GAAA,cAAA,UAAA,CAAU,KAAK,WAAW,uBAAuB;GAGjD,MAAM,QAAQ,MAAMC,+BAAAA,cAA4B,UAAU;IACxD,WAAW,KAAK;IAChB,QAAQ;IACR,UAAU;GACZ,CAAC;GAID,MAAM,OAAOC,oBAAAA,eAAe,UAAU,IAAI;GAE1C,OAAO;IACL,MAF0B,OAAO;KAAE,GAAG;KAAO;IAAK,IAAI;IAGtD,QAAQ;KACN,KAAK,MAAM;KACX,OAAO,MAAM;KACb,gBAAgB;KAChB,GAAGC,oBAAAA,WAAW;MACZ,YAAY,MAAM,MAAM;MACxB,aAAa,MAAM,MAAM;KAC3B,CAAC;IACH;GACF;EACF;EACA,MAAM,kBAAkB,EAAE,MAAM,GAAG,aAAa,GAAG,UAAmC;GACpF,CAAA,GAAA,cAAA,UAAA,CAAU,YAAY,MAAM,kBAAkB;GAC9C,MAAM,EAAE,aAAa;GACrB,MAAM,EAAE,SAAS;GACjB,MAAM,WAAW,MAAMJ,+BAAAA,0BAA0B;IAAE;IAAM;IAAU,GAAG;GAAO,CAAC;GAC9E,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,MAAM,QAAS,MAAM,SAAS,KAAK;GACnC,MAAM,EAAE,MAAM,WAAW,MAAM,KAAK,YAAY,OAAO,EAAE,UAAU,OAAO,SAAS,CAAC;GAGpF,MAAM,WAAWI,oBAAAA,gBAAgB,YAAY,QAAQ;GACrD,MAAM,OAAOD,oBAAAA,WAAW;IACtB,YAAY,UAAU,aAAa,KAAA;IACnC,aAAa,UAAU,cAAc,KAAA;IACrC,aAAa,UAAU,cAAc,KAAA;IACrC,UAAU,UAAU,YAAY,KAAA;GAClC,CAAC;GAKD,OAAO;IAAE;IAAO,UAAA;KAHd,MAAM,WAAW;MAAE,GAAI;MAAwB;KAAS,IAAK;KAC7D,QAAQ;MAAE,GAAG;MAAQ,GAAG;KAAK;IAER;GAAE;EAC3B;CACF;AACF;AAEA,MAAa,QAAQ,oBAAoB"}
@@ -1,25 +1,32 @@
1
- import { NativeCredential, OAuth2Token, Provider } from "../types.cjs";
1
+ import { NativeCredential, OAuth2Token, PersonNameComponents, Provider } from "../types.cjs";
2
+ import { AppleUser } from "../name.cjs";
2
3
  //#region src/oauth2/provider/apple.d.ts
3
4
  export declare function createAppleProvider(): Provider;
4
5
  export declare const apple: Provider;
5
- export interface AppleUserInfo {
6
+ export interface AppleIdToken {
7
+ iss: string;
8
+ aud: string;
9
+ exp: number;
10
+ iat: number;
6
11
  sub: string;
7
12
  email: string;
8
13
  email_verified: true | 'true';
9
14
  is_private_email: boolean;
10
- real_user_status: number;
11
- name: string;
12
- picture: string;
13
- user?: {
14
- name: {
15
- firstName: string;
16
- lastName: string;
17
- };
18
- email: string;
19
- };
15
+ /** Native flow, first authorization only: 0 unsupported, 1 unknown, 2 likely real. */
16
+ real_user_status?: number;
20
17
  nonce?: string;
21
18
  nonce_supported?: boolean;
22
19
  }
20
+ /**
21
+ * `userInfo.data` of an Apple login: the id token's claims, plus the unsigned name the client
22
+ * passed on the first authorization, cleaned (see `nameClaims` for the standard claims made of it).
23
+ */
24
+ export interface AppleUserInfo extends AppleIdToken {
25
+ /** Web flow: Apple's form_post `user`, as Apple names it. */
26
+ user?: AppleUser;
27
+ /** Native flow: the login body's `fullName`, which alone keeps `namePrefix` / `nameSuffix`. */
28
+ fullName?: PersonNameComponents;
29
+ }
23
30
  export interface AppleToken extends OAuth2Token {
24
31
  access_token: string;
25
32
  token_type: string;
@@ -31,8 +38,6 @@ export interface AppleNativeCredential extends NativeCredential {
31
38
  state: string;
32
39
  code: string;
33
40
  id_token: string;
34
- user: string;
35
- email?: string;
36
41
  }
37
42
  export interface AppleErrorResponse {
38
43
  error: 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope';
@@ -1 +1 @@
1
- {"version":3,"file":"apple.d.cts","names":[],"sources":["../../../src/oauth2/provider/apple.ts"],"mappings":";;wBAMgB,uBAAuB;qBAwD1B,OAAK;iBAGD;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;MAAQ;MAAmB;;IAC3B;;EAEF;EACA;;iBAKe,mBAAmB;EAClC;EACA;EACA;EACA;EACA;;iBAGe,8BAA8B;EAC7C;EACA;EACA;EAEA;EACA;;iBAIe;EACf"}
1
+ {"version":3,"file":"apple.d.cts","names":[],"sources":["../../../src/oauth2/provider/apple.ts"],"mappings":";;;wBAiBgB,uBAAuB;qBAgF1B,OAAK;iBAID;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;;;;;;iBAOe,sBAAsB;;EAErC,OAAO;;EAEP,WAAW;;iBAKI,mBAAmB;EAClC;EACA;EACA;EACA;EACA;;iBAGe,8BAA8B;EAC7C;EACA;EACA;;iBAIe;EACf"}
@@ -1,25 +1,32 @@
1
- import { NativeCredential, OAuth2Token, Provider } from "../types.mjs";
1
+ import { NativeCredential, OAuth2Token, PersonNameComponents, Provider } from "../types.mjs";
2
+ import { AppleUser } from "../name.mjs";
2
3
  //#region src/oauth2/provider/apple.d.ts
3
4
  export declare function createAppleProvider(): Provider;
4
5
  export declare const apple: Provider;
5
- export interface AppleUserInfo {
6
+ export interface AppleIdToken {
7
+ iss: string;
8
+ aud: string;
9
+ exp: number;
10
+ iat: number;
6
11
  sub: string;
7
12
  email: string;
8
13
  email_verified: true | 'true';
9
14
  is_private_email: boolean;
10
- real_user_status: number;
11
- name: string;
12
- picture: string;
13
- user?: {
14
- name: {
15
- firstName: string;
16
- lastName: string;
17
- };
18
- email: string;
19
- };
15
+ /** Native flow, first authorization only: 0 unsupported, 1 unknown, 2 likely real. */
16
+ real_user_status?: number;
20
17
  nonce?: string;
21
18
  nonce_supported?: boolean;
22
19
  }
20
+ /**
21
+ * `userInfo.data` of an Apple login: the id token's claims, plus the unsigned name the client
22
+ * passed on the first authorization, cleaned (see `nameClaims` for the standard claims made of it).
23
+ */
24
+ export interface AppleUserInfo extends AppleIdToken {
25
+ /** Web flow: Apple's form_post `user`, as Apple names it. */
26
+ user?: AppleUser;
27
+ /** Native flow: the login body's `fullName`, which alone keeps `namePrefix` / `nameSuffix`. */
28
+ fullName?: PersonNameComponents;
29
+ }
23
30
  export interface AppleToken extends OAuth2Token {
24
31
  access_token: string;
25
32
  token_type: string;
@@ -31,8 +38,6 @@ export interface AppleNativeCredential extends NativeCredential {
31
38
  state: string;
32
39
  code: string;
33
40
  id_token: string;
34
- user: string;
35
- email?: string;
36
41
  }
37
42
  export interface AppleErrorResponse {
38
43
  error: 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope';
@@ -1 +1 @@
1
- {"version":3,"file":"apple.d.mts","names":[],"sources":["../../../src/oauth2/provider/apple.ts"],"mappings":";;wBAMgB,uBAAuB;qBAwD1B,OAAK;iBAGD;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;MAAQ;MAAmB;;IAC3B;;EAEF;EACA;;iBAKe,mBAAmB;EAClC;EACA;EACA;EACA;EACA;;iBAGe,8BAA8B;EAC7C;EACA;EACA;EAEA;EACA;;iBAIe;EACf"}
1
+ {"version":3,"file":"apple.d.mts","names":[],"sources":["../../../src/oauth2/provider/apple.ts"],"mappings":";;;wBAiBgB,uBAAuB;qBAgF1B,OAAK;iBAID;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;;;;;;iBAOe,sBAAsB;;EAErC,OAAO;;EAEP,WAAW;;iBAKI,mBAAmB;EAClC;EACA;EACA;EACA;EACA;;iBAGe,8BAA8B;EAC7C;EACA;EACA;;iBAIe;EACf"}
@@ -1,7 +1,10 @@
1
1
  import { OAuth2Error } from "../error.mjs";
2
+ import { cleanPersonName, nameClaims, parseAppleUser } from "../name.mjs";
2
3
  import { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from "./common.mjs";
3
4
  import { invariant } from "@shware/utils";
4
5
  //#region src/oauth2/provider/apple.ts
6
+ /** `iss` of every Apple id token (https://appleid.apple.com/.well-known/openid-configuration). */
7
+ const APPLE_ISSUER = "https://appleid.apple.com";
5
8
  function createAppleProvider() {
6
9
  return {
7
10
  authorizationUri: "https://appleid.apple.com/auth/authorize?response_mode=form_post",
@@ -31,20 +34,28 @@ function createAppleProvider() {
31
34
  }
32
35
  return await response.json();
33
36
  },
34
- async getUserInfo({ id_token }) {
37
+ async getUserInfo({ id_token }, { clientId, response }) {
35
38
  invariant(id_token, "id_token is required");
36
39
  invariant(this.jwkSetUri, "jwkSetUri is required");
37
- const data = await verifyIdToken(id_token, this.jwkSetUri);
40
+ const token = await verifyIdToken(id_token, {
41
+ jwkSetUri: this.jwkSetUri,
42
+ issuer: APPLE_ISSUER,
43
+ audience: clientId
44
+ });
45
+ const user = parseAppleUser(response?.user);
38
46
  return {
39
- data,
47
+ data: user ? {
48
+ ...token,
49
+ user
50
+ } : token,
40
51
  claims: {
41
- sub: data.sub,
42
- name: data.name,
43
- email: data.email,
52
+ sub: token.sub,
53
+ email: token.email,
44
54
  email_verified: true,
45
- picture: data.picture,
46
- given_name: data.user?.name.firstName,
47
- family_name: data.user?.name.lastName
55
+ ...nameClaims({
56
+ given_name: user?.name?.firstName,
57
+ family_name: user?.name?.lastName
58
+ })
48
59
  }
49
60
  };
50
61
  },
@@ -62,9 +73,26 @@ function createAppleProvider() {
62
73
  throw new OAuth2Error(response.status, error);
63
74
  }
64
75
  const token = await response.json();
76
+ const { data, claims } = await this.getUserInfo(token, { clientId: params.clientId });
77
+ const fullName = cleanPersonName(credentials.fullName);
78
+ const name = nameClaims({
79
+ given_name: fullName?.givenName ?? void 0,
80
+ family_name: fullName?.familyName ?? void 0,
81
+ middle_name: fullName?.middleName ?? void 0,
82
+ nickname: fullName?.nickname ?? void 0
83
+ });
65
84
  return {
66
85
  token,
67
- userInfo: await this.getUserInfo(token)
86
+ userInfo: {
87
+ data: fullName ? {
88
+ ...data,
89
+ fullName
90
+ } : data,
91
+ claims: {
92
+ ...claims,
93
+ ...name
94
+ }
95
+ }
68
96
  };
69
97
  }
70
98
  };