agl 1.0.17 → 21.0.2

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 (277) hide show
  1. package/.smartconfig.json +55 -0
  2. package/changelog.md +1026 -0
  3. package/cli.js +14 -0
  4. package/dist_serve/bundle.js +44778 -0
  5. package/dist_serve/index.html +13 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/classes.authmodels.d.ts +126 -0
  9. package/dist_ts/classes.authmodels.js +1422 -0
  10. package/dist_ts/classes.authstore.d.ts +160 -0
  11. package/dist_ts/classes.authstore.js +2483 -0
  12. package/dist_ts/classes.browserresourcehost.d.ts +85 -0
  13. package/dist_ts/classes.browserresourcehost.js +912 -0
  14. package/dist_ts/classes.cli.d.ts +21 -0
  15. package/dist_ts/classes.cli.js +1082 -0
  16. package/dist_ts/classes.config.d.ts +23 -0
  17. package/dist_ts/classes.config.js +267 -0
  18. package/dist_ts/classes.controller.d.ts +523 -0
  19. package/dist_ts/classes.controller.js +10611 -0
  20. package/dist_ts/classes.controllermanagement.d.ts +32 -0
  21. package/dist_ts/classes.controllermanagement.js +222 -0
  22. package/dist_ts/classes.draftmanager.d.ts +30 -0
  23. package/dist_ts/classes.draftmanager.js +179 -0
  24. package/dist_ts/classes.embeddeddb.d.ts +30 -0
  25. package/dist_ts/classes.embeddeddb.js +159 -0
  26. package/dist_ts/classes.flexframedtransport.d.ts +56 -0
  27. package/dist_ts/classes.flexframedtransport.js +275 -0
  28. package/dist_ts/classes.flexmodels.private.d.ts +231 -0
  29. package/dist_ts/classes.flexmodels.private.js +985 -0
  30. package/dist_ts/classes.flexmodels.public.d.ts +207 -0
  31. package/dist_ts/classes.flexmodels.public.js +885 -0
  32. package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
  33. package/dist_ts/classes.flexprojectionreader.js +580 -0
  34. package/dist_ts/classes.flexservice.d.ts +196 -0
  35. package/dist_ts/classes.flexservice.js +3704 -0
  36. package/dist_ts/classes.flexstore.d.ts +174 -0
  37. package/dist_ts/classes.flexstore.js +3400 -0
  38. package/dist_ts/classes.flexsupervisor.d.ts +105 -0
  39. package/dist_ts/classes.flexsupervisor.js +1191 -0
  40. package/dist_ts/classes.gitreversion.d.ts +431 -0
  41. package/dist_ts/classes.gitreversion.js +10592 -0
  42. package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
  43. package/dist_ts/classes.harnessoperationmanager.js +602 -0
  44. package/dist_ts/classes.opencodeclient.d.ts +146 -0
  45. package/dist_ts/classes.opencodeclient.js +3003 -0
  46. package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
  47. package/dist_ts/classes.opencodesupervisor.js +1003 -0
  48. package/dist_ts/classes.passkeymanager.d.ts +34 -0
  49. package/dist_ts/classes.passkeymanager.js +456 -0
  50. package/dist_ts/classes.processinspection.d.ts +50 -0
  51. package/dist_ts/classes.processinspection.js +484 -0
  52. package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
  53. package/dist_ts/classes.resourcecoordinator.js +294 -0
  54. package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
  55. package/dist_ts/classes.sessionintelligencereader.js +40 -0
  56. package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
  57. package/dist_ts/classes.systemmetricssampler.js +168 -0
  58. package/dist_ts/classes.terminalmanager.d.ts +95 -0
  59. package/dist_ts/classes.terminalmanager.js +981 -0
  60. package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
  61. package/dist_ts/classes.terminalresourcehost.js +84 -0
  62. package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
  63. package/dist_ts/classes.upgradecoordinator.js +2155 -0
  64. package/dist_ts/classes.upgradetransaction.d.ts +92 -0
  65. package/dist_ts/classes.upgradetransaction.js +2125 -0
  66. package/dist_ts/classes.uploadmanager.d.ts +24 -0
  67. package/dist_ts/classes.uploadmanager.js +210 -0
  68. package/dist_ts/classes.validation.d.ts +91 -0
  69. package/dist_ts/classes.validation.js +1087 -0
  70. package/dist_ts/flexharness.child.d.ts +1 -0
  71. package/dist_ts/flexharness.child.js +28 -0
  72. package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
  73. package/dist_ts/functions.canonicaldirectory.js +144 -0
  74. package/dist_ts/functions.controllerdataroot.d.ts +15 -0
  75. package/dist_ts/functions.controllerdataroot.js +76 -0
  76. package/dist_ts/functions.embeddeddb.d.ts +2 -0
  77. package/dist_ts/functions.embeddeddb.js +21 -0
  78. package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
  79. package/dist_ts/functions.flexcredentialstore.js +93 -0
  80. package/dist_ts/functions.opencodeauth.d.ts +3 -0
  81. package/dist_ts/functions.opencodeauth.js +31 -0
  82. package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
  83. package/dist_ts/functions.runtimeenvironment.js +79 -0
  84. package/dist_ts/functions.upgradefollower.d.ts +3 -0
  85. package/dist_ts/functions.upgradefollower.js +95 -0
  86. package/dist_ts/index.d.ts +24 -0
  87. package/dist_ts/index.js +25 -0
  88. package/dist_ts/interfaces.auth.d.ts +181 -0
  89. package/dist_ts/interfaces.auth.js +82 -0
  90. package/dist_ts/interfaces.config.d.ts +34 -0
  91. package/dist_ts/interfaces.config.js +2 -0
  92. package/dist_ts/interfaces.flexipc.d.ts +939 -0
  93. package/dist_ts/interfaces.flexipc.js +1492 -0
  94. package/dist_ts/interfaces.gitreversion.d.ts +246 -0
  95. package/dist_ts/interfaces.gitreversion.js +2 -0
  96. package/dist_ts/interfaces.lifecycle.d.ts +37 -0
  97. package/dist_ts/interfaces.lifecycle.js +2 -0
  98. package/dist_ts/interfaces.opencode.d.ts +101 -0
  99. package/dist_ts/interfaces.opencode.js +3 -0
  100. package/dist_ts/interfaces.projects.d.ts +156 -0
  101. package/dist_ts/interfaces.projects.js +5 -0
  102. package/dist_ts/plugins.d.ts +36 -0
  103. package/dist_ts/plugins.js +40 -0
  104. package/dist_ts_interfaces/index.d.ts +2 -0
  105. package/dist_ts_interfaces/index.js +3 -0
  106. package/dist_ts_interfaces/interfaces.d.ts +747 -0
  107. package/dist_ts_interfaces/interfaces.js +47 -0
  108. package/dist_ts_interfaces/plugins.d.ts +4 -0
  109. package/dist_ts_interfaces/plugins.js +3 -0
  110. package/dist_ts_interfaces/requests.d.ts +806 -0
  111. package/dist_ts_interfaces/requests.js +2 -0
  112. package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
  113. package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
  114. package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
  115. package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
  116. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
  117. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
  118. package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
  119. package/dist_ts_migration/classes.migrationrunner.js +34 -0
  120. package/dist_ts_migration/index.d.ts +12 -0
  121. package/dist_ts_migration/index.js +13 -0
  122. package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
  123. package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
  124. package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
  125. package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
  126. package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
  127. package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
  128. package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
  129. package/dist_ts_migration/v18_resourcelayout.js +23 -0
  130. package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
  131. package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
  132. package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
  133. package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
  134. package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
  135. package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
  136. package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
  137. package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
  138. package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
  139. package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
  140. package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
  141. package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
  142. package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
  143. package/dist_ts_migration/v6_protocoldocuments.js +138 -0
  144. package/html/index.html +13 -0
  145. package/{LICENSE → license.md} +2 -2
  146. package/package.json +86 -48
  147. package/readme.md +480 -0
  148. package/readme.plan.md +188 -0
  149. package/ts/00_commitinfo_data.ts +8 -0
  150. package/ts/classes.authmodels.ts +1245 -0
  151. package/ts/classes.authstore.ts +3274 -0
  152. package/ts/classes.browserresourcehost.ts +1205 -0
  153. package/ts/classes.cli.ts +1254 -0
  154. package/ts/classes.config.ts +351 -0
  155. package/ts/classes.controller.ts +14426 -0
  156. package/ts/classes.controllermanagement.ts +330 -0
  157. package/ts/classes.draftmanager.ts +232 -0
  158. package/ts/classes.embeddeddb.ts +166 -0
  159. package/ts/classes.flexframedtransport.ts +283 -0
  160. package/ts/classes.flexmodels.private.ts +823 -0
  161. package/ts/classes.flexmodels.public.ts +732 -0
  162. package/ts/classes.flexprojectionreader.ts +816 -0
  163. package/ts/classes.flexservice.ts +4302 -0
  164. package/ts/classes.flexstore.ts +4565 -0
  165. package/ts/classes.flexsupervisor.ts +1453 -0
  166. package/ts/classes.gitreversion.ts +13353 -0
  167. package/ts/classes.harnessoperationmanager.ts +870 -0
  168. package/ts/classes.opencodeclient.ts +4110 -0
  169. package/ts/classes.opencodesupervisor.ts +1280 -0
  170. package/ts/classes.passkeymanager.ts +570 -0
  171. package/ts/classes.processinspection.ts +623 -0
  172. package/ts/classes.resourcecoordinator.ts +436 -0
  173. package/ts/classes.sessionintelligencereader.ts +52 -0
  174. package/ts/classes.systemmetricssampler.ts +215 -0
  175. package/ts/classes.terminalmanager.ts +1207 -0
  176. package/ts/classes.terminalresourcehost.ts +122 -0
  177. package/ts/classes.upgradecoordinator.ts +2675 -0
  178. package/ts/classes.upgradetransaction.ts +2900 -0
  179. package/ts/classes.uploadmanager.ts +240 -0
  180. package/ts/classes.validation.ts +1409 -0
  181. package/ts/flexharness.child.ts +35 -0
  182. package/ts/functions.canonicaldirectory.ts +213 -0
  183. package/ts/functions.controllerdataroot.ts +107 -0
  184. package/ts/functions.embeddeddb.ts +22 -0
  185. package/ts/functions.flexcredentialstore.ts +128 -0
  186. package/ts/functions.opencodeauth.ts +34 -0
  187. package/ts/functions.runtimeenvironment.ts +90 -0
  188. package/ts/functions.upgradefollower.ts +115 -0
  189. package/ts/index.ts +36 -6
  190. package/ts/interfaces.auth.ts +316 -0
  191. package/ts/interfaces.config.ts +37 -0
  192. package/ts/interfaces.flexipc.ts +2586 -0
  193. package/ts/interfaces.gitreversion.ts +336 -0
  194. package/ts/interfaces.lifecycle.ts +46 -0
  195. package/ts/interfaces.opencode.ts +153 -0
  196. package/ts/interfaces.projects.ts +182 -0
  197. package/ts/plugins.ts +64 -0
  198. package/ts_interfaces/index.ts +2 -0
  199. package/ts_interfaces/interfaces.ts +827 -0
  200. package/ts_interfaces/plugins.ts +15 -0
  201. package/ts_interfaces/requests.ts +1186 -0
  202. package/ts_migration/classes.documentmigrationrunner.ts +32 -0
  203. package/ts_migration/classes.flexmigrationrunner.ts +33 -0
  204. package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
  205. package/ts_migration/classes.migrationrunner.ts +40 -0
  206. package/ts_migration/index.ts +12 -0
  207. package/ts_migration/v10_flexharnessv3migration.ts +35 -0
  208. package/ts_migration/v13_flexprojectionorder.ts +33 -0
  209. package/ts_migration/v16_flexprovidercredentials.ts +196 -0
  210. package/ts_migration/v18_resourcelayout.ts +55 -0
  211. package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
  212. package/ts_migration/v19_flexprojectmanagement.ts +28 -0
  213. package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
  214. package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
  215. package/ts_migration/v2_controllerdataroot.ts +2349 -0
  216. package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
  217. package/ts_migration/v6_protocoldocuments.ts +198 -0
  218. package/ts_web/00_commitinfo_data.ts +8 -0
  219. package/ts_web/classes.controllersocketclient.ts +517 -0
  220. package/ts_web/classes.sessiondraftsync.ts +390 -0
  221. package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
  222. package/ts_web/index.ts +2 -0
  223. package/ts_web/plugins.ts +31 -0
  224. package/.gitlab-ci.yml +0 -71
  225. package/.npmignore +0 -5
  226. package/README.md +0 -40
  227. package/dist/cli.js +0 -2
  228. package/dist/index.d.ts +0 -0
  229. package/dist/index.js +0 -9
  230. package/dist/mod_bundle/index.d.ts +0 -1
  231. package/dist/mod_bundle/index.js +0 -18
  232. package/dist/mod_bundle/mod.plugins.d.ts +0 -4
  233. package/dist/mod_bundle/mod.plugins.js +0 -11
  234. package/dist/mod_install/index.d.ts +0 -4
  235. package/dist/mod_install/index.js +0 -21
  236. package/dist/mod_install/mod.plugins.d.ts +0 -1
  237. package/dist/mod_install/mod.plugins.js +0 -7
  238. package/dist/mod_standard/index.d.ts +0 -4
  239. package/dist/mod_standard/index.js +0 -21
  240. package/dist/mod_standard/mod.browser.d.ts +0 -6
  241. package/dist/mod_standard/mod.browser.js +0 -14
  242. package/dist/mod_standard/mod.clean.d.ts +0 -4
  243. package/dist/mod_standard/mod.clean.js +0 -12
  244. package/dist/mod_standard/mod.compile.d.ts +0 -4
  245. package/dist/mod_standard/mod.compile.js +0 -21
  246. package/dist/mod_standard/mod.plugins.d.ts +0 -8
  247. package/dist/mod_standard/mod.plugins.js +0 -19
  248. package/dist/mod_standard/mod.test.d.ts +0 -4
  249. package/dist/mod_standard/mod.test.js +0 -12
  250. package/dist/plr.cli.d.ts +0 -3
  251. package/dist/plr.cli.js +0 -57
  252. package/dist/plr.config.d.ts +0 -11
  253. package/dist/plr.config.js +0 -26
  254. package/dist/plr.mods.d.ts +0 -17
  255. package/dist/plr.mods.js +0 -17
  256. package/dist/plr.paths.d.ts +0 -19
  257. package/dist/plr.paths.js +0 -26
  258. package/dist/plr.plugins.d.ts +0 -8
  259. package/dist/plr.plugins.js +0 -16
  260. package/npmextra.json +0 -10
  261. package/ts/mod_bundle/index.ts +0 -20
  262. package/ts/mod_bundle/mod.plugins.ts +0 -9
  263. package/ts/mod_install/index.ts +0 -20
  264. package/ts/mod_install/mod.plugins.ts +0 -1
  265. package/ts/mod_standard/index.ts +0 -23
  266. package/ts/mod_standard/mod.browser.ts +0 -17
  267. package/ts/mod_standard/mod.clean.ts +0 -13
  268. package/ts/mod_standard/mod.compile.ts +0 -23
  269. package/ts/mod_standard/mod.plugins.ts +0 -17
  270. package/ts/mod_standard/mod.test.ts +0 -13
  271. package/ts/plr.cli.ts +0 -60
  272. package/ts/plr.config.ts +0 -36
  273. package/ts/plr.mods.ts +0 -25
  274. package/ts/plr.paths.ts +0 -29
  275. package/ts/plr.plugins.ts +0 -17
  276. package/tslint.json +0 -3
  277. package/yarn.lock +0 -3464
@@ -0,0 +1,570 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ AuthError,
4
+ type IAuthenticationBeginOptions,
5
+ type IAuthenticationBeginResult,
6
+ type IAuthenticationFinishOptions,
7
+ type IAuthenticationResult,
8
+ type IPasskeyManagerOptions,
9
+ type ISetupBeginOptions,
10
+ type ISetupBeginResult,
11
+ type ISetupFinishOptions,
12
+ type IStoredPasskeyCredential,
13
+ type IWebAuthnCeremonyDocument,
14
+ type IWebAuthnServerImplementation,
15
+ type IWebAuthnSetupCeremonyDocument,
16
+ } from './interfaces.auth.js';
17
+
18
+ const setupCeremonyLifetimeMs = 5 * 60 * 1000;
19
+ const authenticationCeremonyLifetimeMs = 2 * 60 * 1000;
20
+ const maxAuthenticationVerificationAttemptsPerWindow = 128;
21
+ const base64Url32BytePattern = /^[A-Za-z0-9_-]{43}$/;
22
+
23
+ const defaultWebAuthn: IWebAuthnServerImplementation = {
24
+ generateRegistrationOptions: plugins.simplewebauthnServer.generateRegistrationOptions,
25
+ verifyRegistrationResponse: plugins.simplewebauthnServer.verifyRegistrationResponse,
26
+ generateAuthenticationOptions: plugins.simplewebauthnServer.generateAuthenticationOptions,
27
+ verifyAuthenticationResponse: plugins.simplewebauthnServer.verifyAuthenticationResponse,
28
+ };
29
+
30
+ const requirePositiveLimit = (
31
+ valueArg: number,
32
+ labelArg: string,
33
+ maximumArg = 10_000,
34
+ ): number => {
35
+ if (!Number.isSafeInteger(valueArg) || valueArg < 1 || valueArg > maximumArg) {
36
+ throw new Error(`${labelArg} must be a positive safe integer no greater than ${maximumArg}.`);
37
+ }
38
+ return valueArg;
39
+ };
40
+
41
+ const requirePeerId = (peerIdArg: string): string => {
42
+ if (
43
+ typeof peerIdArg !== 'string'
44
+ || peerIdArg.length === 0
45
+ || peerIdArg.length > 512
46
+ || peerIdArg.trim() !== peerIdArg
47
+ ) {
48
+ throw new AuthError('peer_mismatch', 'The connection identity is malformed.');
49
+ }
50
+ return peerIdArg;
51
+ };
52
+
53
+ const requireOrigin = (originArg: string): string => {
54
+ if (typeof originArg !== 'string' || originArg.length === 0 || originArg.length > 2048) {
55
+ throw new AuthError('origin_mismatch', 'The request origin is malformed.');
56
+ }
57
+ try {
58
+ const origin = new URL(originArg);
59
+ if (
60
+ origin.origin !== originArg
61
+ || (origin.protocol !== 'http:' && origin.protocol !== 'https:')
62
+ || origin.username.length > 0
63
+ || origin.password.length > 0
64
+ || origin.pathname !== '/'
65
+ || origin.search.length > 0
66
+ || origin.hash.length > 0
67
+ ) {
68
+ throw new Error('not an origin');
69
+ }
70
+ } catch {
71
+ throw new AuthError('origin_mismatch', 'The request origin is malformed.');
72
+ }
73
+ return originArg;
74
+ };
75
+
76
+ const requireCeremonyId = (ceremonyIdArg: string): string => {
77
+ if (typeof ceremonyIdArg !== 'string' || !base64Url32BytePattern.test(ceremonyIdArg)) {
78
+ throw new AuthError('ceremony_invalid', 'The WebAuthn ceremony identifier is malformed.');
79
+ }
80
+ return ceremonyIdArg;
81
+ };
82
+
83
+ const randomBase64Url = (): string => plugins.crypto.randomBytes(32).toString('base64url');
84
+
85
+ const base64UrlValuesEqual = (leftArg: string, rightArg: string): boolean => {
86
+ if (!base64Url32BytePattern.test(leftArg) || !base64Url32BytePattern.test(rightArg)) {
87
+ return false;
88
+ }
89
+ const left = Buffer.from(leftArg, 'base64url');
90
+ const right = Buffer.from(rightArg, 'base64url');
91
+ return left.byteLength === right.byteLength && plugins.crypto.timingSafeEqual(left, right);
92
+ };
93
+
94
+ export class PasskeyManager {
95
+ private readonly store: IPasskeyManagerOptions['store'];
96
+ private readonly relyingPartyName: string;
97
+ private readonly webAuthn: IWebAuthnServerImplementation;
98
+ private readonly now: () => Date;
99
+ private readonly setupAttemptLimit: number;
100
+ private readonly setupAttemptWindowMs: number;
101
+ private readonly authenticationAttemptLimitPerPeer: number;
102
+ private readonly authenticationAttemptWindowMs: number;
103
+ private readonly maxActiveSetupCeremonies: number;
104
+ private readonly maxActiveAuthenticationCeremonies: number;
105
+ private readonly setupAttemptTimestamps: number[] = [];
106
+ private readonly authenticationVerificationAttemptTimestamps: number[] = [];
107
+ private readonly authenticationAttemptTimestampsByPeer = new Map<string, number[]>();
108
+ private ceremonyCreationTail: Promise<void> = Promise.resolve();
109
+
110
+ constructor(optionsArg: IPasskeyManagerOptions) {
111
+ this.store = optionsArg.store;
112
+ this.relyingPartyName = optionsArg.relyingPartyName ?? 'Agent Gateway Layer';
113
+ if (this.relyingPartyName.trim().length === 0 || this.relyingPartyName.length > 128) {
114
+ throw new Error('relyingPartyName must contain 1-128 characters.');
115
+ }
116
+ this.webAuthn = optionsArg.webAuthn ?? defaultWebAuthn;
117
+ this.now = optionsArg.now ?? (() => new Date());
118
+ this.setupAttemptLimit = requirePositiveLimit(
119
+ optionsArg.setupAttemptLimit ?? 10,
120
+ 'setupAttemptLimit',
121
+ );
122
+ this.setupAttemptWindowMs = requirePositiveLimit(
123
+ optionsArg.setupAttemptWindowMs ?? 60_000,
124
+ 'setupAttemptWindowMs',
125
+ 24 * 60 * 60 * 1000,
126
+ );
127
+ this.authenticationAttemptLimitPerPeer = requirePositiveLimit(
128
+ optionsArg.authenticationAttemptLimitPerPeer ?? 6,
129
+ 'authenticationAttemptLimitPerPeer',
130
+ );
131
+ this.authenticationAttemptWindowMs = requirePositiveLimit(
132
+ optionsArg.authenticationAttemptWindowMs ?? 60_000,
133
+ 'authenticationAttemptWindowMs',
134
+ 24 * 60 * 60 * 1000,
135
+ );
136
+ this.maxActiveSetupCeremonies = requirePositiveLimit(
137
+ optionsArg.maxActiveSetupCeremonies ?? 16,
138
+ 'maxActiveSetupCeremonies',
139
+ );
140
+ this.maxActiveAuthenticationCeremonies = requirePositiveLimit(
141
+ optionsArg.maxActiveAuthenticationCeremonies ?? 64,
142
+ 'maxActiveAuthenticationCeremonies',
143
+ );
144
+ }
145
+
146
+ public async getAuthState(): Promise<{
147
+ state: 'setupRequired' | 'ready';
148
+ setupCodeExpired: boolean;
149
+ }> {
150
+ const state = await this.store.getState();
151
+ return {
152
+ state: state.authState,
153
+ setupCodeExpired: state.setup !== null
154
+ && state.setup.expiresAt.getTime() <= this.currentTime().getTime(),
155
+ };
156
+ }
157
+
158
+ public async beginSetup(optionsArg: ISetupBeginOptions): Promise<ISetupBeginResult> {
159
+ const peerId = requirePeerId(optionsArg.peerId);
160
+ const origin = requireOrigin(optionsArg.origin);
161
+ return this.withCeremonyCreationLock(async () => {
162
+ const now = this.currentTime();
163
+ await this.assertCeremonyCapacity('setup', now);
164
+ const state = await this.store.getState();
165
+ this.assertConfiguredOrigin(origin, state.config.publicOrigin);
166
+ // the attempt budget must burn BEFORE the code is checked — a limiter
167
+ // that only counts correct codes throttles the legitimate operator and
168
+ // leaves guessing unmetered
169
+ this.recordSetupAttempt(now);
170
+ await this.store.assertSetupAuthority(optionsArg.setupCode, undefined, now);
171
+ if (!state.setup) {
172
+ throw new AuthError('setup_unavailable', 'Passkey setup is no longer available.');
173
+ }
174
+
175
+ const registrationOptions = await this.webAuthn.generateRegistrationOptions({
176
+ rpName: this.relyingPartyName,
177
+ rpID: state.config.rpId,
178
+ userName: 'agl',
179
+ userDisplayName: 'agl',
180
+ userID: Buffer.from(state.webauthnUserId, 'base64url'),
181
+ timeout: setupCeremonyLifetimeMs,
182
+ attestationType: 'none',
183
+ excludeCredentials: [],
184
+ authenticatorSelection: {
185
+ residentKey: 'required',
186
+ requireResidentKey: true,
187
+ userVerification: 'required',
188
+ },
189
+ });
190
+ const ceremonyId = randomBase64Url();
191
+ const expiresAt = new Date(Math.min(
192
+ now.getTime() + setupCeremonyLifetimeMs,
193
+ state.setup.expiresAt.getTime(),
194
+ ));
195
+ const ceremony: IWebAuthnSetupCeremonyDocument = {
196
+ id: ceremonyId,
197
+ kind: 'setup',
198
+ state: 'pending',
199
+ peerId,
200
+ challenge: registrationOptions.challenge,
201
+ origin,
202
+ rpId: state.config.rpId,
203
+ userId: state.webauthnUserId,
204
+ setupGeneration: state.setup.generation,
205
+ setupHash: state.setup.hash,
206
+ createdAt: now,
207
+ expiresAt,
208
+ };
209
+ await this.store.createCeremony(ceremony);
210
+ return { ceremonyId, options: registrationOptions };
211
+ });
212
+ }
213
+
214
+ public async finishSetup(optionsArg: ISetupFinishOptions): Promise<IAuthenticationResult> {
215
+ const peerId = requirePeerId(optionsArg.peerId);
216
+ const origin = requireOrigin(optionsArg.origin);
217
+ const ceremonyId = requireCeremonyId(optionsArg.ceremonyId);
218
+ const now = this.currentTime();
219
+ const stateBefore = await this.store.getState();
220
+ this.assertConfiguredOrigin(origin, stateBefore.config.publicOrigin);
221
+ // burn the attempt budget BEFORE consuming the ceremony: an exhausted
222
+ // budget must reject without destroying a still-valid ceremony, otherwise
223
+ // the user's authenticator keeps a credential the store never accepted
224
+ this.recordSetupAttempt(now);
225
+ const ceremony = await this.store.consumeCeremony({
226
+ ceremonyId,
227
+ kind: 'setup',
228
+ peerId,
229
+ origin,
230
+ rpId: stateBefore.config.rpId,
231
+ userId: stateBefore.webauthnUserId,
232
+ now,
233
+ });
234
+ if (
235
+ ceremony.kind !== 'setup'
236
+ || typeof ceremony.setupGeneration !== 'string'
237
+ || typeof ceremony.setupHash !== 'string'
238
+ ) {
239
+ throw new AuthError('ceremony_invalid', 'The WebAuthn ceremony has the wrong kind.');
240
+ }
241
+ const setupCeremony: IWebAuthnSetupCeremonyDocument = {
242
+ ...ceremony,
243
+ kind: 'setup',
244
+ setupGeneration: ceremony.setupGeneration,
245
+ setupHash: ceremony.setupHash,
246
+ };
247
+ await this.store.assertSetupAuthority(optionsArg.setupCode, setupCeremony, now);
248
+
249
+ let verification: Awaited<ReturnType<
250
+ IWebAuthnServerImplementation['verifyRegistrationResponse']
251
+ >>;
252
+ try {
253
+ verification = await this.webAuthn.verifyRegistrationResponse({
254
+ response: optionsArg.response,
255
+ expectedChallenge: ceremony.challenge,
256
+ expectedOrigin: ceremony.origin,
257
+ expectedRPID: ceremony.rpId,
258
+ requireUserPresence: true,
259
+ requireUserVerification: true,
260
+ });
261
+ } catch (errorArg) {
262
+ throw new AuthError('verification_failed', 'Passkey registration verification failed.', {
263
+ cause: errorArg,
264
+ });
265
+ }
266
+ if (
267
+ !verification.verified
268
+ || !verification.registrationInfo.userVerified
269
+ || verification.registrationInfo.credential.id !== optionsArg.response.id
270
+ ) {
271
+ throw new AuthError('verification_failed', 'Passkey registration verification failed.');
272
+ }
273
+
274
+ const verifiedCredential = verification.registrationInfo.credential;
275
+ const storedCredential: IStoredPasskeyCredential = {
276
+ id: verifiedCredential.id,
277
+ publicKey: Buffer.from(verifiedCredential.publicKey).toString('base64url'),
278
+ counter: verifiedCredential.counter,
279
+ transports: [...(verifiedCredential.transports ?? optionsArg.response.response.transports ?? [])],
280
+ deviceType: verification.registrationInfo.credentialDeviceType,
281
+ backedUp: verification.registrationInfo.credentialBackedUp,
282
+ createdAt: now,
283
+ counterUpdateId: randomBase64Url(),
284
+ };
285
+ await this.store.commitFirstCredential(setupCeremony, storedCredential);
286
+ return { authenticated: true, credentialId: storedCredential.id };
287
+ }
288
+
289
+ public async preflightSetupFinish(optionsArg: ISetupFinishOptions): Promise<void> {
290
+ const peerId = requirePeerId(optionsArg.peerId);
291
+ const origin = requireOrigin(optionsArg.origin);
292
+ const ceremonyId = requireCeremonyId(optionsArg.ceremonyId);
293
+ const now = this.currentTime();
294
+ const state = await this.store.getState();
295
+ this.assertConfiguredOrigin(origin, state.config.publicOrigin);
296
+ const ceremony = await this.store.assertPendingCeremony({
297
+ ceremonyId,
298
+ kind: 'setup',
299
+ peerId,
300
+ origin,
301
+ rpId: state.config.rpId,
302
+ userId: state.webauthnUserId,
303
+ now,
304
+ });
305
+ if (
306
+ ceremony.kind !== 'setup'
307
+ || typeof ceremony.setupGeneration !== 'string'
308
+ || typeof ceremony.setupHash !== 'string'
309
+ ) {
310
+ throw new AuthError('ceremony_invalid', 'The WebAuthn ceremony has the wrong kind.');
311
+ }
312
+ // preflight is the first place a wrong code arrives in the controller
313
+ // flow, so it must burn the attempt budget too
314
+ this.recordSetupAttempt(now);
315
+ await this.store.assertSetupAuthority(optionsArg.setupCode, {
316
+ ...ceremony,
317
+ kind: 'setup',
318
+ setupGeneration: ceremony.setupGeneration,
319
+ setupHash: ceremony.setupHash,
320
+ }, now);
321
+ }
322
+
323
+ public async beginAuthentication(
324
+ optionsArg: IAuthenticationBeginOptions,
325
+ ): Promise<IAuthenticationBeginResult> {
326
+ const peerId = requirePeerId(optionsArg.peerId);
327
+ const origin = requireOrigin(optionsArg.origin);
328
+ return this.withCeremonyCreationLock(async () => {
329
+ const now = this.currentTime();
330
+ await this.assertCeremonyCapacity('authentication', now);
331
+ const state = await this.store.getState();
332
+ this.assertConfiguredOrigin(origin, state.config.publicOrigin);
333
+ if (state.authState !== 'ready' || state.credentials.length === 0) {
334
+ throw new AuthError('setup_unavailable', 'Passkey authentication is not configured.');
335
+ }
336
+ this.recordAuthenticationAttempt(now, peerId, false);
337
+ const authenticationOptions = await this.webAuthn.generateAuthenticationOptions({
338
+ rpID: state.config.rpId,
339
+ timeout: authenticationCeremonyLifetimeMs,
340
+ userVerification: 'required',
341
+ });
342
+ const ceremonyId = randomBase64Url();
343
+ const ceremony: IWebAuthnCeremonyDocument = {
344
+ id: ceremonyId,
345
+ kind: 'authentication',
346
+ state: 'pending',
347
+ peerId,
348
+ challenge: authenticationOptions.challenge,
349
+ origin,
350
+ rpId: state.config.rpId,
351
+ userId: state.webauthnUserId,
352
+ createdAt: now,
353
+ expiresAt: new Date(now.getTime() + authenticationCeremonyLifetimeMs),
354
+ };
355
+ await this.store.createCeremony(ceremony);
356
+ return { ceremonyId, options: authenticationOptions };
357
+ });
358
+ }
359
+
360
+ public async finishAuthentication(
361
+ optionsArg: IAuthenticationFinishOptions,
362
+ ): Promise<IAuthenticationResult> {
363
+ const peerId = requirePeerId(optionsArg.peerId);
364
+ const origin = requireOrigin(optionsArg.origin);
365
+ const ceremonyId = requireCeremonyId(optionsArg.ceremonyId);
366
+ const now = this.currentTime();
367
+ const state = await this.store.getState();
368
+ this.assertConfiguredOrigin(origin, state.config.publicOrigin);
369
+ const ceremony = await this.store.consumeCeremony({
370
+ ceremonyId,
371
+ kind: 'authentication',
372
+ peerId,
373
+ origin,
374
+ rpId: state.config.rpId,
375
+ userId: state.webauthnUserId,
376
+ now,
377
+ });
378
+ if (state.authState !== 'ready' || state.credentials.length === 0) {
379
+ throw new AuthError('setup_unavailable', 'Passkey authentication is not configured.');
380
+ }
381
+ const userHandle = optionsArg.response.response.userHandle;
382
+ if (
383
+ typeof userHandle !== 'string'
384
+ || !base64UrlValuesEqual(userHandle, state.webauthnUserId)
385
+ ) {
386
+ throw new AuthError('verification_failed', 'The passkey user handle is invalid.');
387
+ }
388
+ const credential = state.credentials.find(
389
+ (entry) => entry.id === optionsArg.response.id,
390
+ );
391
+ if (!credential) {
392
+ throw new AuthError('credential_not_found', 'The passkey credential is unknown.');
393
+ }
394
+ this.recordAuthenticationAttempt(now, peerId, true);
395
+
396
+ let verification: Awaited<ReturnType<
397
+ IWebAuthnServerImplementation['verifyAuthenticationResponse']
398
+ >>;
399
+ try {
400
+ verification = await this.webAuthn.verifyAuthenticationResponse({
401
+ response: optionsArg.response,
402
+ expectedChallenge: ceremony.challenge,
403
+ expectedOrigin: ceremony.origin,
404
+ expectedRPID: ceremony.rpId,
405
+ credential: {
406
+ id: credential.id,
407
+ publicKey: Buffer.from(credential.publicKey, 'base64url'),
408
+ counter: credential.counter,
409
+ transports: [...credential.transports],
410
+ },
411
+ requireUserVerification: true,
412
+ advancedFIDOConfig: {
413
+ userVerification: 'required',
414
+ },
415
+ });
416
+ } catch (errorArg) {
417
+ throw new AuthError('verification_failed', 'Passkey authentication verification failed.', {
418
+ cause: errorArg,
419
+ });
420
+ }
421
+ if (
422
+ !verification.verified
423
+ || !verification.authenticationInfo.userVerified
424
+ || verification.authenticationInfo.credentialID !== credential.id
425
+ || verification.authenticationInfo.origin !== ceremony.origin
426
+ || verification.authenticationInfo.rpID !== ceremony.rpId
427
+ ) {
428
+ throw new AuthError('verification_failed', 'Passkey authentication verification failed.');
429
+ }
430
+ await this.store.updateCounter(
431
+ credential.id,
432
+ credential.counter,
433
+ verification.authenticationInfo.newCounter,
434
+ );
435
+ return { authenticated: true, credentialId: credential.id };
436
+ }
437
+
438
+ public async preflightAuthenticationFinish(
439
+ optionsArg: IAuthenticationFinishOptions,
440
+ ): Promise<void> {
441
+ const peerId = requirePeerId(optionsArg.peerId);
442
+ const origin = requireOrigin(optionsArg.origin);
443
+ const ceremonyId = requireCeremonyId(optionsArg.ceremonyId);
444
+ const now = this.currentTime();
445
+ const state = await this.store.getState();
446
+ this.assertConfiguredOrigin(origin, state.config.publicOrigin);
447
+ if (state.authState !== 'ready' || state.credentials.length === 0) {
448
+ throw new AuthError('setup_unavailable', 'Passkey authentication is not configured.');
449
+ }
450
+ const userHandle = optionsArg.response.response.userHandle;
451
+ if (
452
+ typeof userHandle !== 'string'
453
+ || !base64UrlValuesEqual(userHandle, state.webauthnUserId)
454
+ ) {
455
+ throw new AuthError('verification_failed', 'The passkey user handle is invalid.');
456
+ }
457
+ if (!state.credentials.some((entry) => entry.id === optionsArg.response.id)) {
458
+ throw new AuthError('credential_not_found', 'The passkey credential is unknown.');
459
+ }
460
+ await this.store.assertPendingCeremony({
461
+ ceremonyId,
462
+ kind: 'authentication',
463
+ peerId,
464
+ origin,
465
+ rpId: state.config.rpId,
466
+ userId: state.webauthnUserId,
467
+ now,
468
+ });
469
+ }
470
+
471
+ private currentTime(): Date {
472
+ const now = this.now();
473
+ if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
474
+ throw new Error('The PasskeyManager clock returned an invalid Date.');
475
+ }
476
+ return new Date(now);
477
+ }
478
+
479
+ private recordSetupAttempt(nowArg: Date): void {
480
+ const cutoff = nowArg.getTime() - this.setupAttemptWindowMs;
481
+ while (
482
+ this.setupAttemptTimestamps.length > 0
483
+ && this.setupAttemptTimestamps[0] <= cutoff
484
+ ) {
485
+ this.setupAttemptTimestamps.shift();
486
+ }
487
+ if (this.setupAttemptTimestamps.length >= this.setupAttemptLimit) {
488
+ throw new AuthError('rate_limited', 'Too many setup attempts. Try again later.');
489
+ }
490
+ this.setupAttemptTimestamps.push(nowArg.getTime());
491
+ }
492
+
493
+ private recordAuthenticationAttempt(
494
+ nowArg: Date,
495
+ peerIdArg: string,
496
+ includeGlobalVerificationLimitArg: boolean,
497
+ ): void {
498
+ const now = nowArg.getTime();
499
+ const cutoff = now - this.authenticationAttemptWindowMs;
500
+ while (
501
+ this.authenticationVerificationAttemptTimestamps.length > 0
502
+ && this.authenticationVerificationAttemptTimestamps[0] <= cutoff
503
+ ) {
504
+ this.authenticationVerificationAttemptTimestamps.shift();
505
+ }
506
+ for (const [peerId, timestamps] of this.authenticationAttemptTimestampsByPeer) {
507
+ while (timestamps.length > 0 && timestamps[0] <= cutoff) timestamps.shift();
508
+ if (timestamps.length === 0) this.authenticationAttemptTimestampsByPeer.delete(peerId);
509
+ }
510
+
511
+ const peerTimestamps = this.authenticationAttemptTimestampsByPeer.get(peerIdArg) ?? [];
512
+ if (peerTimestamps.length >= this.authenticationAttemptLimitPerPeer) {
513
+ throw new AuthError('rate_limited', 'Too many authentication attempts. Try again later.');
514
+ }
515
+ if (
516
+ includeGlobalVerificationLimitArg
517
+ && this.authenticationVerificationAttemptTimestamps.length
518
+ >= maxAuthenticationVerificationAttemptsPerWindow
519
+ ) {
520
+ throw new AuthError('rate_limited', 'Too many passkey verification attempts. Try again later.');
521
+ }
522
+ if (
523
+ peerTimestamps.length === 0
524
+ && this.authenticationAttemptTimestampsByPeer.size
525
+ >= this.maxActiveAuthenticationCeremonies * 2
526
+ ) {
527
+ throw new AuthError('rate_limited', 'Too many authentication peers. Try again later.');
528
+ }
529
+ if (includeGlobalVerificationLimitArg) {
530
+ this.authenticationVerificationAttemptTimestamps.push(now);
531
+ }
532
+ peerTimestamps.push(now);
533
+ this.authenticationAttemptTimestampsByPeer.set(peerIdArg, peerTimestamps);
534
+ }
535
+
536
+ private async assertCeremonyCapacity(
537
+ kindArg: 'setup' | 'authentication',
538
+ nowArg: Date,
539
+ ): Promise<void> {
540
+ const maximum = kindArg === 'setup'
541
+ ? this.maxActiveSetupCeremonies
542
+ : this.maxActiveAuthenticationCeremonies;
543
+ const active = await this.store.countActiveCeremonies(kindArg, nowArg);
544
+ if (active >= maximum) {
545
+ throw new AuthError('ceremony_limit', `Too many active ${kindArg} ceremonies.`);
546
+ }
547
+ }
548
+
549
+ private assertConfiguredOrigin(actualArg: string, expectedArg: string): void {
550
+ if (actualArg !== expectedArg) {
551
+ throw new AuthError('origin_mismatch', 'The request origin is not trusted.');
552
+ }
553
+ }
554
+
555
+ private async withCeremonyCreationLock<TResult>(
556
+ functionArg: () => Promise<TResult>,
557
+ ): Promise<TResult> {
558
+ const previous = this.ceremonyCreationTail;
559
+ let release!: () => void;
560
+ this.ceremonyCreationTail = new Promise<void>((resolve) => {
561
+ release = resolve;
562
+ });
563
+ await previous;
564
+ try {
565
+ return await functionArg();
566
+ } finally {
567
+ release();
568
+ }
569
+ }
570
+ }