@spfn/auth 0.3.0-beta.26 → 0.3.0-beta.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,12 +104,16 @@ Import it for its side-effect (it self-registers); it must run before the proxy
104
104
  // app/api/rpc/[routeName]/route.ts
105
105
  import '@spfn/auth/nextjs/api'; // side-effect: registers auth interceptors
106
106
  import { createRpcProxy } from '@spfn/core/nextjs/server';
107
- import { authRouteMap } from '@spfn/auth';
108
107
  import { routeMap } from '@/generated/route-map';
109
108
 
110
- export const { GET, POST } = createRpcProxy({ routeMap: { ...routeMap, ...authRouteMap } });
109
+ export const { GET, POST } = createRpcProxy({ routeMap });
111
110
  ```
112
111
 
112
+ No auth route map is merged: the generated `routeMap` carries the routes of every package
113
+ router the app router mounts with `.packages()`, `authRouter`'s included. `authRouteMap` is
114
+ still exported and `{ ...routeMap, ...authRouteMap }` is still harmless — the two hold the
115
+ same entries — but it is a no-op.
116
+
113
117
  ### 4. Run migrations
114
118
 
115
119
  ```bash
@@ -215,6 +219,13 @@ routes use `.skip(['auth'])`; the rest require `Authorization: Bearer <client-si
215
219
  | `getDeviceAuthInfo` | POST `/_auth/device/info` | yes | what device is asking, so the approval screen can show it |
216
220
  | `approveDeviceAuth` | POST `/_auth/device/approve` | yes | let the waiting device in |
217
221
  | `denyDeviceAuth` | POST `/_auth/device/deny` | yes | refuse it |
222
+ | `issueDeviceLink` | POST `/_auth/device/link/issue` | yes | show a code a new device can come in by — see [Device link](#device-link) |
223
+ | `redeemDeviceLink` | POST `/_auth/device/link/redeem` | public | the new device parks its key on that code and gets the match number to show |
224
+ | `getDeviceLinkStatus` | POST `/_auth/device/link/status` | issuing key | where the link stands; the device and three numbers once redeemed |
225
+ | `confirmDeviceLink` | POST `/_auth/device/link/confirm` | issuing key | pick the number the new device shows |
226
+ | `denyDeviceLink` | POST `/_auth/device/link/deny` | issuing key | refuse the new device |
227
+ | `cancelDeviceLink` | POST `/_auth/device/link/cancel` | issuing key | close the link before anyone is let in |
228
+ | `pollDeviceLink` | POST `/_auth/device/link/poll` | public | ask whether the issuer picked; the approved answer *is* the login |
218
229
  | `passkeyRegisterOptions` | POST `/_auth/passkeys/register/options` | yes | begin enrolling a passkey — see [Passkeys](#passkeys-webauthn) |
219
230
  | `passkeyRegisterVerify` | POST `/_auth/passkeys/register/verify` | yes | verify the attestation and keep the credential |
220
231
  | `passkeyLoginOptions` | POST `/_auth/passkeys/login/options` | public | begin a passkey sign-in; takes no identifier |
@@ -499,6 +510,31 @@ const answer = await authApi.pollDeviceAuth.call({ body: { deviceCode } });
499
510
  // → { status: 'approved', userId, publicId, email?, phone?, passwordChangeRequired }
500
511
  ```
501
512
 
513
+ Or long-poll: send `waitMillis` and the server holds a pending request until the owner
514
+ answers or the wait runs out, so the device learns of an approval the moment it is made
515
+ instead of at its next tick.
516
+
517
+ ```typescript
518
+ let answer;
519
+
520
+ do
521
+ {
522
+ // Held up to 20s (the server's maxWaitMs caps it). A pending answer takes the time
523
+ // already waited off intervalMillis — 0 after a full wait, so ask again at once.
524
+ // An error ends the loop, as before.
525
+ answer = await authApi.pollDeviceAuth.call({ body: { deviceCode, waitMillis: 20_000 } });
526
+
527
+ if (answer.status === 'pending' && answer.intervalMillis > 0)
528
+ {
529
+ await new Promise(resolve => setTimeout(resolve, answer.intervalMillis));
530
+ }
531
+ }
532
+ while (answer.status === 'pending');
533
+ ```
534
+
535
+ Keep the loop's sleep on `intervalMillis > 0`. It covers a server that answered without
536
+ waiting — an older one that ignores the field — so the loop never spins.
537
+
502
538
  ```typescript
503
539
  // On the signed-in device — the user typed the code they read off the other screen.
504
540
  const asking = await authApi.getDeviceAuthInfo.call({ body: { userCode } });
@@ -553,26 +589,175 @@ two ways in are indistinguishable.
553
589
  who cannot authenticate, so `publicKey`, `keyId` and `fingerprint` carry length limits —
554
590
  generous next to a real key (an RSA-2048 SPKI is 392 base64 characters against a 2048 limit)
555
591
  and small next to the megabyte that would otherwise sit in a table no job clears.
592
+ - **A long poll holds no transaction.** The wait is route middleware in front of the poll's
593
+ `Transactional()`, so a waiting device does not pin a pooled connection, and the answer is
594
+ judged inside the transaction exactly as a poll without `waitMillis` is — same atomicity,
595
+ same database-error answers. Approve, deny and a global revocation wake a poll parked in the
596
+ same process after they commit. A poll parked on another instance re-reads its record every
597
+ second, so an approval committed elsewhere reaches it within about a second. A device that
598
+ hangs up mid-wait is not judged, so an approval it can no longer hear waits for its next poll;
599
+ at most three polls wait on one code at a time — a fourth is answered at once; and a server
600
+ that starts shutting down ends every wait with a pending answer rather than a cut connection.
556
601
  - **Clock skew cannot affect this.** Every timestamp in the decision is the server's. The
557
602
  `expiresAtMillis` in the start response is for the waiting device's countdown display, and
558
603
  nothing the client believes about the time reaches the server's judgement.
559
604
 
560
- Two knobs, both announced to the waiting device in the start response and therefore resolved
561
- at lifecycle time rather than read per call:
605
+ Three knobs, resolved at lifecycle time rather than read per call — the first two are
606
+ announced to the waiting device in the start response:
562
607
 
563
608
  ```typescript
564
609
  createAuthLifecycle({
565
610
  deviceAuth: {
566
611
  ttlMs: 10 * 60 * 1000, // how long a code lives. default 10 minutes
567
612
  intervalMs: 5000, // poll interval the server asks for. default 5s
613
+ maxWaitMs: 20_000, // longest a long poll is held. default 20s
568
614
  },
569
615
  })
570
616
  ```
571
617
 
618
+ Keep `maxWaitMs` under the idle timeout of every proxy and load balancer in front of the
619
+ server. A long poll cut off by one reaches the device as a network error, not as a pending
620
+ answer — Google Cloud's load balancer closes a backend request at 30 seconds by default.
621
+
572
622
  No job sweeps the table. Rows are judged by `expiresAt` whenever they are read or moved, so a
573
623
  stale row authorizes nothing; it only keeps its user code out of circulation, and 31⁸ codes do
574
624
  not run out.
575
625
 
626
+ ### Device link
627
+
628
+ Device-code login the other way round: the device that is already signed in shows the code,
629
+ and the new one reads it. This is the natural way onto a phone — the signed-in device is a
630
+ laptop with a screen, and the phone has a camera. The signed-in device (the *issuer*) asks for a
631
+ code and shows it, as text and as a QR its own client draws. The new device reads it, sends it
632
+ with its fresh public key, and shows a two-digit number. The issuer is shown the new device and
633
+ three numbers, and taps the one on the new device's screen. The new device's next poll is its
634
+ login.
635
+
636
+ ```typescript
637
+ // On the signed-in device — the issuer. Every call below is signed with its key.
638
+ const { linkId, userCode, expiresAtMillis } = await authApi.issueDeviceLink.call({});
639
+
640
+ // Show `userCode` (XXXX-XXXX) as text and in a QR code, then wait for a device to use it.
641
+ let link;
642
+
643
+ do
644
+ {
645
+ // Held up to 20s while nobody has redeemed the code (deviceAuth.maxWaitMs caps it).
646
+ link = await authApi.getDeviceLinkStatus.call({ body: { linkId, waitMillis: 20_000 } });
647
+ }
648
+ while (link.status === 'issued');
649
+ // → { status: 'redeemed', deviceName?, platform?, fingerprintPrefix, redeemedAtMillis,
650
+ // choices: [37, 82, 15], expiresAtMillis }
651
+
652
+ // Show the device and the three numbers; the person taps the one on the new device.
653
+ await authApi.confirmDeviceLink.call({ body: { linkId, choice: tapped } });
654
+ // or authApi.denyDeviceLink.call({ body: { linkId } }); closing the screen:
655
+ // authApi.cancelDeviceLink.call({ body: { linkId } })
656
+ ```
657
+
658
+ ```typescript
659
+ // On the new device — it has no key on file, so both calls are public.
660
+ const { deviceCode, matchNumber, expiresAtMillis, intervalMillis } =
661
+ await authApi.redeemDeviceLink.call({ body: {
662
+ userCode, // read from the QR, or typed
663
+ publicKey, keyId, fingerprint, algorithm: 'ES256',
664
+ deviceName: 'Pocket phone', platform: 'ios',
665
+ } });
666
+
667
+ // Show `matchNumber` large ("tap 37 on your computer"), then long-poll as device-code does.
668
+ let answer;
669
+
670
+ do
671
+ {
672
+ answer = await authApi.pollDeviceLink.call({ body: { deviceCode, waitMillis: 20_000 } });
673
+
674
+ if (answer.status === 'pending' && answer.intervalMillis > 0)
675
+ {
676
+ await new Promise(resolve => setTimeout(resolve, answer.intervalMillis));
677
+ }
678
+ }
679
+ while (answer.status === 'pending');
680
+ // → { status: 'approved', userId, publicId, email?, phone?, passwordChangeRequired }
681
+ ```
682
+
683
+ The approved answer is the one `pollDeviceAuth` and `login` return, produced by the same
684
+ completion — account status checks, key registration, the login event — so a client cannot tell
685
+ which way in it took. Its key is registered under the issuer's account with channel
686
+ `device-link` on `auth.device.registered`.
687
+
688
+ | state ↓ call → | redeem | status | confirm | deny | cancel | poll |
689
+ | --- | --- | --- | --- | --- | --- | --- |
690
+ | issued | → redeemed | `issued` | 409 NotRedeemed | 409 NotRedeemed | → expired | — |
691
+ | redeemed | 404 | device + `choices` | right number → approved; wrong → denied + 400 WrongMatch | → denied | → expired | `pending` |
692
+ | approved | 404 | `approved` | 409 AlreadyHandled | 409 AlreadyHandled | 409 AlreadyHandled | login, → consumed |
693
+ | denied | 404 | `denied` | 409 AlreadyHandled | 409 AlreadyHandled | 409 AlreadyHandled | 403 Denied |
694
+ | consumed | 404 | `consumed` | 409 AlreadyHandled | 409 AlreadyHandled | 409 AlreadyHandled | 404 |
695
+ | dead (TTL, cancelled, replaced, issuer signed out) | 400 Expired | 400 Expired | 400 Expired | 400 Expired | 400 Expired | 400 Expired |
696
+ | unknown, or another key's link | 404 | 404 | 404 | 404 | 404 | 404 |
697
+
698
+ Error names are `DeviceLink` + the cell: `DeviceLinkNotFoundError` (404),
699
+ `DeviceLinkExpiredError` (400), `DeviceLinkWrongMatchError` (400), `DeviceLinkDeniedError` (403),
700
+ `DeviceLinkNotRedeemedError` (409), `DeviceLinkAlreadyHandledError` (409). A consumed link stays
701
+ 404 to the new device after its TTL, for device-code login's reason.
702
+
703
+ - **Single use.** A code is redeemed once, by one device: redeem moves the link from `issued` and
704
+ nowhere else, so of two devices sending the same code exactly one parks its key, and the other
705
+ is told the code does not exist. The approval is collected once, the same way: of two polls
706
+ arriving together, one registers the key and the other gets 404.
707
+ - **Five-minute TTL.** A link code lives 5 minutes (`deviceLink.ttlMs`). Every decision uses the
708
+ server's clock, carried into the statement that moves the link; `expiresAtMillis` is for the
709
+ countdown on screen and nothing else. Issuing again from the same device expires the previous
710
+ link, so there is one live link per issuing key.
711
+ - **Only the issuing key can confirm.** Status, confirm, deny and cancel are bound to the key that
712
+ signed `issue` — not merely the account. Another device of the same account, or another account,
713
+ is answered 404, exactly as for a link that never existed. The link also dies with that key: once
714
+ it is revoked, signed out or past its own expiry, every call on the link answers expired — and
715
+ confirm and the poll that registers the key re-check the issuing key inside the statement that
716
+ moves the link, under a lock on the key row, so a sign-out landing at the same moment is never
717
+ read around. A global revocation (`revoke-all`, even the kind that spares the calling device, a
718
+ password change, a deletion request) expires the account's links as well.
719
+ - **The match number, and its limit, stated plainly.** Redeem answers the new device with a number
720
+ from 10 to 99; the issuer is shown it among two other distinct numbers, in an order fixed when the
721
+ code was redeemed. The number is what ties the device the issuer is looking at to the device that
722
+ redeemed the code: someone who read the code off the issuer's screen redeems it on a phone the
723
+ issuer cannot see, so the issuer has no number to match. **A person who taps without looking at
724
+ the new device picks the right number one time in three.** That is the whole of the odds, because
725
+ one wrong pick denies the link — there is no second try, and the new device is told it was refused.
726
+ Typing the number would be stronger; three to tap is the trade taken for a flow people finish.
727
+ - **The new device's key is registered only after a confirm.** It is parked in
728
+ `spfn_auth.device_links`, not in `user_public_keys`, until the poll after the right pick moves it
729
+ over. A key parked on a link that was denied, cancelled, or expired can never sign anything and
730
+ can never be collected.
731
+ - **A redeemed, spent or never-issued code answer alike** (`DeviceLinkNotFoundError`, 404), so a
732
+ guesser cannot learn a code was real. A code that died of age answers 400.
733
+ - **Rate limits.** `issue` per account (10/min, and 50/min per IP); `redeem` and `poll` per IP
734
+ (10/min and 30/min); `status` per account (30/min, 150/min per IP) and `confirm` / `deny` /
735
+ `cancel` per account (10/min, 50/min per IP) — the device-code policies' sizes.
736
+ - **The device code is stored only as a SHA-256 hash**, returned once, as in device-code login.
737
+ Nothing in the flow logs a user code, device code, match number or key.
738
+ - **Long polls hold no transaction.** Both `status` (the issuer, while the link waits on the other
739
+ device) and `poll` (the new device, while it waits on the pick) take `waitMillis` and wait exactly
740
+ as the device-code poll does: ahead of the transaction, capped at `deviceAuth.maxWaitMs`, at most
741
+ three requests waiting on one link, a re-read every second for a change committed on another
742
+ instance, every transition waking both after commit, and a shutdown ending every wait with the
743
+ current answer.
744
+ - **`redeem` bounds what it stores** exactly as `device/start` does — it is the other route that
745
+ takes key material from a caller who cannot authenticate.
746
+
747
+ One knob of its own; the poll interval and long-poll cap are `deviceAuth`'s:
748
+
749
+ ```typescript
750
+ createAuthLifecycle({
751
+ deviceLink: {
752
+ ttlMs: 5 * 60 * 1000, // how long a link code lives. default 5 minutes
753
+ },
754
+ })
755
+ ```
756
+
757
+ The mobile contract carries the new device's half — `auth.deviceLink.redeem` and
758
+ `auth.deviceLink.poll`, since contract 0.13.2. The issuer's five routes run on the signed-in
759
+ device and are not on that surface.
760
+
576
761
  ### Registered devices (key management)
577
762
 
578
763
  A [passkey](#passkeys-webauthn) is **not** one of these keys: it is a credential that proves
@@ -2129,8 +2314,8 @@ whoever opened a link in a mailbox, so it is the notice to send the owner.
2129
2314
 
2130
2315
  `authDeviceRegisteredEvent` (`auth.device.registered`) fires after commit whenever a device key is
2131
2316
  registered on an account, on every channel that registers one — `channel` says which: `register`,
2132
- `signup-link`, `invitation`, `password`, `oauth`, `oauth-native`, `device-code`, `password-reset`
2133
- or `passkey`. It carries `userId`, `keyId`, `algorithm`, a 12-character `fingerprintPrefix`,
2317
+ `signup-link`, `invitation`, `password`, `oauth`, `oauth-native`, `device-code`, `device-link`,
2318
+ `password-reset` or `passkey`. It carries `userId`, `keyId`, `algorithm`, a 12-character `fingerprintPrefix`,
2134
2319
  `createdAtMillis`, and whatever the registration knew about the device: `deviceName?`, `platform?`,
2135
2320
  `ip?` and `userAgent?` — the web OAuth callback has neither label, because the sealed state does
2136
2321
  not carry them. Subscribe to tell the owner a device was added: a login event says a session began
@@ -2478,6 +2663,8 @@ Every operation in the exported bundle carries `since` — the contract version
2478
2663
  | `auth.keys.list`, `auth.keys.revoke`, `auth.keys.revokeAll` | 0.4.1 |
2479
2664
  | `core.time` | 0.9.0 |
2480
2665
  | `auth.device.start`, `auth.device.poll`, `auth.device.info`, `auth.device.approve`, `auth.device.deny` | 0.10.0 |
2666
+ | `auth.mfa.verify`, `auth.mfa.status` | 0.13.0 |
2667
+ | `auth.deviceLink.redeem`, `auth.deviceLink.poll` | 0.13.2 |
2481
2668
 
2482
2669
  - **This is history, not policy.** The mobile contract's compatibility policy is `allOrNothing`: one
2483
2670
  contract version passes or refuses the whole surface, so these three fields change no verdict here.
@@ -3231,9 +3418,8 @@ export type AppRouter = typeof appRouter;
3231
3418
  // app/api/rpc/[routeName]/route.ts
3232
3419
  import '@spfn/auth/nextjs/api';
3233
3420
  import { createRpcProxy } from '@spfn/core/nextjs/server';
3234
- import { authRouteMap } from '@spfn/auth';
3235
- import { routeMap } from '@/generated/route-map';
3236
- export const { GET, POST } = createRpcProxy({ routeMap: { ...routeMap, ...authRouteMap } });
3421
+ import { routeMap } from '@/generated/route-map'; // already holds authRouter's routes
3422
+ export const { GET, POST } = createRpcProxy({ routeMap });
3237
3423
 
3238
3424
  // any client component
3239
3425
  import { authApi } from '@spfn/auth';
@@ -398,7 +398,7 @@ declare function getClientProofReplayStore(): ClientProofReplayStore;
398
398
  */
399
399
 
400
400
  interface ContractOperation {
401
- id: 'auth.clientProof.handshake' | 'echo.send' | 'items.list' | 'auth.enroll.register' | 'auth.enroll.login' | 'auth.enroll.oauthNative' | 'auth.mfa.verify' | 'auth.mfa.status' | 'auth.keys.rotate' | 'auth.keys.list' | 'auth.keys.revoke' | 'auth.keys.revokeAll' | 'auth.device.start' | 'auth.device.poll' | 'auth.device.info' | 'auth.device.approve' | 'auth.device.deny' | typeof CORE_TIME_OPERATION_ID;
401
+ id: 'auth.clientProof.handshake' | 'echo.send' | 'items.list' | 'auth.enroll.register' | 'auth.enroll.login' | 'auth.enroll.oauthNative' | 'auth.mfa.verify' | 'auth.mfa.status' | 'auth.keys.rotate' | 'auth.keys.list' | 'auth.keys.revoke' | 'auth.keys.revokeAll' | 'auth.device.start' | 'auth.device.poll' | 'auth.device.info' | 'auth.device.approve' | 'auth.device.deny' | 'auth.deviceLink.redeem' | 'auth.deviceLink.poll' | typeof CORE_TIME_OPERATION_ID;
402
402
  method: 'GET' | 'POST';
403
403
  path: string;
404
404
  /**
@@ -482,6 +482,13 @@ declare const CONTRACT_OPERATIONS: readonly ContractOperation[];
482
482
  * which is what lets the server read the approving account from the caller
483
483
  * rather than from the request body.
484
484
  *
485
+ * Device link is device-code login's mirror image, and only the new device's
486
+ * half of it is here: `redeem` and `poll`, both unproven for the same reason.
487
+ * The issuer's five operations — issue, status, confirm, deny, cancel — are
488
+ * bound to the key that signed `issue`, and the device that runs them is the
489
+ * already-signed-in one showing a code, not the phone this contract generates
490
+ * a client for. They stay REST routes, as the `mfa/*` enrolment routes do.
491
+ *
485
492
  * `auth.mfa.verify` is unproven for the same reason the sign-ins are: the key it
486
493
  * activates is not usable until it succeeds, so there is nothing to sign the
487
494
  * call with. It is the only way to finish a sign-in that answered
@@ -1153,6 +1153,28 @@ var AUTH_SURFACE_OPERATIONS = [
1153
1153
  requestType: "DenyDeviceAuthRequest",
1154
1154
  summary: "Refuses the waiting device. Answers 204 with no body, so it names no response type.",
1155
1155
  since: "0.10.0"
1156
+ },
1157
+ {
1158
+ id: "auth.deviceLink.redeem",
1159
+ method: "POST",
1160
+ path: "/_auth/device/link/redeem",
1161
+ authProfile: "none",
1162
+ requiresSession: false,
1163
+ requestType: "RedeemDeviceLinkRequest",
1164
+ responseType: "RedeemDeviceLinkResponse",
1165
+ summary: "Parks a new device's public key on the code a signed-in device shows, and returns the match number to show.",
1166
+ since: "0.13.2"
1167
+ },
1168
+ {
1169
+ id: "auth.deviceLink.poll",
1170
+ method: "POST",
1171
+ path: "/_auth/device/link/poll",
1172
+ authProfile: "none",
1173
+ requiresSession: false,
1174
+ requestType: "PollDeviceLinkRequest",
1175
+ responseType: "PollDeviceAuthResponse",
1176
+ summary: "Asks whether the issuer picked the match number; the approved answer is the login it produced.",
1177
+ since: "0.13.2"
1156
1178
  }
1157
1179
  ];
1158
1180
  var ContractTypeError = class extends Error {
@@ -1419,7 +1441,7 @@ function isAppKind(kind) {
1419
1441
  }
1420
1442
 
1421
1443
  // src/server/client-proof/contract-bundle.ts
1422
- var CONTRACT_VERSION = "0.13.0";
1444
+ var CONTRACT_VERSION = "0.13.2";
1423
1445
  var CONTRACT_MAJOR = 0;
1424
1446
  var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
1425
1447
  function required(name, type) {
@@ -1754,7 +1776,8 @@ var CONTRACT_TYPES = [
1754
1776
  {
1755
1777
  name: "PollDeviceAuthRequest",
1756
1778
  fields: [
1757
- required("deviceCode", "string")
1779
+ required("deviceCode", "string"),
1780
+ optional("waitMillis", "integer")
1758
1781
  ]
1759
1782
  },
1760
1783
  /**
@@ -1815,6 +1838,39 @@ var CONTRACT_TYPES = [
1815
1838
  fields: [
1816
1839
  required("userCode", "string")
1817
1840
  ]
1841
+ },
1842
+ /**
1843
+ * `StartDeviceAuthRequest` with the code it redeems in front: the same key
1844
+ * material under the same bounds, since this is the other place a caller
1845
+ * with nothing to authenticate parks a key before anyone agreed to it.
1846
+ */
1847
+ {
1848
+ name: "RedeemDeviceLinkRequest",
1849
+ fields: [
1850
+ required("userCode", "string"),
1851
+ required("publicKey", "string"),
1852
+ required("keyId", "string"),
1853
+ required("fingerprint", "string"),
1854
+ optional("algorithm", "KeyAlgorithm"),
1855
+ optional("deviceName", "string"),
1856
+ optional("platform", "KeyPlatform")
1857
+ ]
1858
+ },
1859
+ {
1860
+ name: "RedeemDeviceLinkResponse",
1861
+ fields: [
1862
+ required("deviceCode", "string"),
1863
+ required("matchNumber", "integer"),
1864
+ required("expiresAtMillis", "integer"),
1865
+ required("intervalMillis", "integer")
1866
+ ]
1867
+ },
1868
+ {
1869
+ name: "PollDeviceLinkRequest",
1870
+ fields: [
1871
+ required("deviceCode", "string"),
1872
+ optional("waitMillis", "integer")
1873
+ ]
1818
1874
  }
1819
1875
  ];
1820
1876
  var CONTRACT_ENUMS = [