@productcraft/auth 0.6.0 → 0.7.1
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 +52 -2
- package/dist/index.cjs +526 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +929 -155
- package/dist/index.d.ts +929 -155
- package/dist/index.js +519 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -217,6 +217,56 @@ await consumer.auth.signinWithProvider({
|
|
|
217
217
|
|
|
218
218
|
No SDK upgrade required — the per-provider TS enum widens automatically with the next spec refresh.
|
|
219
219
|
|
|
220
|
+
## Passkeys (WebAuthn)
|
|
221
|
+
|
|
222
|
+
Passkeys are switched on per app in the console (relying-party id + the
|
|
223
|
+
site origin in `allowed_redirect_origins`). Once they are, each ceremony
|
|
224
|
+
is one call on `auth.consumer(appSlug).passkeys`. The helpers run
|
|
225
|
+
**in the browser** — they drive `navigator.credentials` — but importing
|
|
226
|
+
the package anywhere is safe; nothing touches a browser global until you
|
|
227
|
+
call them.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { Auth, isPasskeySupported, PasskeyError } from "@productcraft/auth";
|
|
231
|
+
|
|
232
|
+
if (!isPasskeySupported()) hidePasskeyButton(); // false in Node and in insecure contexts
|
|
233
|
+
|
|
234
|
+
// Enrol — signed-in user adds a passkey as an MFA factor
|
|
235
|
+
const me = new Auth({ auth: { type: "bearer", token: accessToken } }).consumer("acme");
|
|
236
|
+
const { factor, recovery_codes } = await me.passkeys.enroll({ label: "MacBook" });
|
|
237
|
+
// show recovery_codes once — they are not retrievable later
|
|
238
|
+
|
|
239
|
+
// Passwordless sign-in — no password, no email; discoverable credential
|
|
240
|
+
const anon = new Auth().consumer("acme");
|
|
241
|
+
const session = await anon.passkeys.signIn({ mediation: "conditional" }); // amr: ["webauthn"]
|
|
242
|
+
|
|
243
|
+
// MFA step of a password sign-in — when signin() answered { mfa_required, mfa_token, factors }
|
|
244
|
+
const session2 = await anon.passkeys.completeMfaChallenge({ mfa_token: signin.mfa_token });
|
|
245
|
+
|
|
246
|
+
// Step-up inside a live session
|
|
247
|
+
const { amr, mfa_at } = await me.passkeys.stepUp();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Each helper does the options call → `navigator.credentials.create()` /
|
|
251
|
+
`.get()` → verify call, with every buffer base64url-encoded in both
|
|
252
|
+
directions and the `public_key` members renamed to the WebAuthn spec's
|
|
253
|
+
camelCase (the Auth wire is snake_case). Browser-side failures reject
|
|
254
|
+
with `PasskeyError` (`code`: `unsupported`, `cancelled`, `timeout`,
|
|
255
|
+
`invalid_state`, `security`, `no_credential`, `unknown`; `cause` holds
|
|
256
|
+
the DOM error); server-side failures stay `AuthHttpError`, so a 401 on
|
|
257
|
+
a forged or replayed assertion is distinguishable from the user
|
|
258
|
+
dismissing the prompt. Pass `signal: AbortSignal.timeout(ms)` to bound
|
|
259
|
+
the prompt yourself.
|
|
260
|
+
|
|
261
|
+
The encoding layer is exported for anyone who wants the pieces:
|
|
262
|
+
`toB64u`, `fromB64u`, `decodeOptions`, `encodeCredential`, plus
|
|
263
|
+
`createPasskeyCredential(public_key)` / `getPasskeyCredential(public_key)`
|
|
264
|
+
which wrap `navigator.credentials` on their own.
|
|
265
|
+
|
|
266
|
+
`signIn()` never sends an identifier: `/auth/passkey/options` takes none
|
|
267
|
+
by design (it would be an account-existence oracle), so sign-in relies on
|
|
268
|
+
a discoverable (resident) credential.
|
|
269
|
+
|
|
220
270
|
## JWT verification
|
|
221
271
|
|
|
222
272
|
Every Auth app publishes a JWKS at `/{appSlug}/v1/.well-known/jwks.json`. The SDK gives you a verified-claims helper and a jose-compatible JWKS resolver.
|
|
@@ -279,7 +329,7 @@ For the per-app boundary, the JWKS is the cryptographic gate (only the app's key
|
|
|
279
329
|
|
|
280
330
|
### Passport integration
|
|
281
331
|
|
|
282
|
-
Use the companion package [`@productcraft/auth-passport`](
|
|
332
|
+
Use the companion package [`@productcraft/auth-passport`](https://github.com/clauderanelagh/productcraft-node/tree/main/packages/auth-passport):
|
|
283
333
|
|
|
284
334
|
```bash
|
|
285
335
|
npm install @productcraft/auth-passport passport-jwt
|
|
@@ -343,4 +393,4 @@ When the spec changes, the nightly `spec-refresh` workflow re-runs codegen and o
|
|
|
343
393
|
|
|
344
394
|
## License
|
|
345
395
|
|
|
346
|
-
[MIT](
|
|
396
|
+
[MIT](https://github.com/clauderanelagh/productcraft-node/blob/main/LICENSE).
|
package/dist/index.cjs
CHANGED
|
@@ -765,11 +765,15 @@ function getApiKeyControllerListApiKeysUrl({
|
|
|
765
765
|
const res = { method: "GET", url: `/v1/apps/${app_id}/api-keys` };
|
|
766
766
|
return res;
|
|
767
767
|
}
|
|
768
|
-
async function apiKeyControllerListApiKeys({
|
|
768
|
+
async function apiKeyControllerListApiKeys({
|
|
769
|
+
appId,
|
|
770
|
+
params
|
|
771
|
+
}, config = {}) {
|
|
769
772
|
const { client: request = client, ...requestConfig } = config;
|
|
770
773
|
const res = await request({
|
|
771
774
|
method: "GET",
|
|
772
775
|
url: getApiKeyControllerListApiKeysUrl({ appId }).url.toString(),
|
|
776
|
+
params,
|
|
773
777
|
...requestConfig
|
|
774
778
|
});
|
|
775
779
|
return res.data;
|
|
@@ -1031,7 +1035,9 @@ async function appAuditControllerGetAuditLogs({
|
|
|
1031
1035
|
limit: params.limit,
|
|
1032
1036
|
cursor: params.cursor,
|
|
1033
1037
|
action: params.action,
|
|
1034
|
-
actor_id: params.actorId
|
|
1038
|
+
actor_id: params.actorId,
|
|
1039
|
+
since: params.since,
|
|
1040
|
+
until: params.until
|
|
1035
1041
|
} : void 0;
|
|
1036
1042
|
const res = await request({
|
|
1037
1043
|
method: "GET",
|
|
@@ -1557,6 +1563,220 @@ async function consumerIdpControllerNativeSignIn({
|
|
|
1557
1563
|
return res.data;
|
|
1558
1564
|
}
|
|
1559
1565
|
|
|
1566
|
+
// src/_generated/clients/consumerAuth/consumerAuthControllerPasskeyOptions.ts
|
|
1567
|
+
function getConsumerAuthControllerPasskeyOptionsUrl({
|
|
1568
|
+
appSlug
|
|
1569
|
+
}) {
|
|
1570
|
+
const app_slug = appSlug;
|
|
1571
|
+
const res = {
|
|
1572
|
+
method: "POST",
|
|
1573
|
+
url: `/${app_slug}/v1/auth/passkey/options`
|
|
1574
|
+
};
|
|
1575
|
+
return res;
|
|
1576
|
+
}
|
|
1577
|
+
async function consumerAuthControllerPasskeyOptions({
|
|
1578
|
+
appSlug,
|
|
1579
|
+
data
|
|
1580
|
+
}, config = {}) {
|
|
1581
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1582
|
+
const requestData = data;
|
|
1583
|
+
const res = await request({
|
|
1584
|
+
method: "POST",
|
|
1585
|
+
url: getConsumerAuthControllerPasskeyOptionsUrl({ appSlug }).url.toString(),
|
|
1586
|
+
data: requestData,
|
|
1587
|
+
...requestConfig
|
|
1588
|
+
});
|
|
1589
|
+
return res.data;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/_generated/clients/consumerAuth/consumerAuthControllerPasskeyVerify.ts
|
|
1593
|
+
function getConsumerAuthControllerPasskeyVerifyUrl({
|
|
1594
|
+
appSlug
|
|
1595
|
+
}) {
|
|
1596
|
+
const app_slug = appSlug;
|
|
1597
|
+
const res = {
|
|
1598
|
+
method: "POST",
|
|
1599
|
+
url: `/${app_slug}/v1/auth/passkey/verify`
|
|
1600
|
+
};
|
|
1601
|
+
return res;
|
|
1602
|
+
}
|
|
1603
|
+
async function consumerAuthControllerPasskeyVerify({
|
|
1604
|
+
appSlug,
|
|
1605
|
+
data
|
|
1606
|
+
}, config = {}) {
|
|
1607
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1608
|
+
const requestData = data;
|
|
1609
|
+
const res = await request({
|
|
1610
|
+
method: "POST",
|
|
1611
|
+
url: getConsumerAuthControllerPasskeyVerifyUrl({ appSlug }).url.toString(),
|
|
1612
|
+
data: requestData,
|
|
1613
|
+
...requestConfig
|
|
1614
|
+
});
|
|
1615
|
+
return res.data;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// src/_generated/clients/consumerAuth/consumerAuthControllerMfaWebauthnOptions.ts
|
|
1619
|
+
function getConsumerAuthControllerMfaWebauthnOptionsUrl({
|
|
1620
|
+
appSlug
|
|
1621
|
+
}) {
|
|
1622
|
+
const app_slug = appSlug;
|
|
1623
|
+
const res = {
|
|
1624
|
+
method: "POST",
|
|
1625
|
+
url: `/${app_slug}/v1/auth/mfa/webauthn/options`
|
|
1626
|
+
};
|
|
1627
|
+
return res;
|
|
1628
|
+
}
|
|
1629
|
+
async function consumerAuthControllerMfaWebauthnOptions({
|
|
1630
|
+
appSlug,
|
|
1631
|
+
data
|
|
1632
|
+
}, config = {}) {
|
|
1633
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1634
|
+
const requestData = data;
|
|
1635
|
+
const res = await request({
|
|
1636
|
+
method: "POST",
|
|
1637
|
+
url: getConsumerAuthControllerMfaWebauthnOptionsUrl({
|
|
1638
|
+
appSlug
|
|
1639
|
+
}).url.toString(),
|
|
1640
|
+
data: requestData,
|
|
1641
|
+
...requestConfig
|
|
1642
|
+
});
|
|
1643
|
+
return res.data;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/_generated/clients/consumerAuth/consumerAuthControllerMfaVerify.ts
|
|
1647
|
+
function getConsumerAuthControllerMfaVerifyUrl({
|
|
1648
|
+
appSlug
|
|
1649
|
+
}) {
|
|
1650
|
+
const app_slug = appSlug;
|
|
1651
|
+
const res = {
|
|
1652
|
+
method: "POST",
|
|
1653
|
+
url: `/${app_slug}/v1/auth/mfa/verify`
|
|
1654
|
+
};
|
|
1655
|
+
return res;
|
|
1656
|
+
}
|
|
1657
|
+
async function consumerAuthControllerMfaVerify({
|
|
1658
|
+
appSlug,
|
|
1659
|
+
data
|
|
1660
|
+
}, config = {}) {
|
|
1661
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1662
|
+
const requestData = data;
|
|
1663
|
+
const res = await request({
|
|
1664
|
+
method: "POST",
|
|
1665
|
+
url: getConsumerAuthControllerMfaVerifyUrl({ appSlug }).url.toString(),
|
|
1666
|
+
data: requestData,
|
|
1667
|
+
...requestConfig
|
|
1668
|
+
});
|
|
1669
|
+
return res.data;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/_generated/clients/consumerMfa/consumerMfaControllerStartEnrollment.ts
|
|
1673
|
+
function getConsumerMfaControllerStartEnrollmentUrl({
|
|
1674
|
+
appSlug
|
|
1675
|
+
}) {
|
|
1676
|
+
const app_slug = appSlug;
|
|
1677
|
+
const res = {
|
|
1678
|
+
method: "POST",
|
|
1679
|
+
url: `/${app_slug}/v1/me/mfa/factors`
|
|
1680
|
+
};
|
|
1681
|
+
return res;
|
|
1682
|
+
}
|
|
1683
|
+
async function consumerMfaControllerStartEnrollment({
|
|
1684
|
+
appSlug,
|
|
1685
|
+
data
|
|
1686
|
+
}, config = {}) {
|
|
1687
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1688
|
+
const requestData = data;
|
|
1689
|
+
const res = await request({
|
|
1690
|
+
method: "POST",
|
|
1691
|
+
url: getConsumerMfaControllerStartEnrollmentUrl({ appSlug }).url.toString(),
|
|
1692
|
+
data: requestData,
|
|
1693
|
+
...requestConfig
|
|
1694
|
+
});
|
|
1695
|
+
return res.data;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/_generated/clients/consumerMfa/consumerMfaControllerConfirmEnrollment.ts
|
|
1699
|
+
function getConsumerMfaControllerConfirmEnrollmentUrl({
|
|
1700
|
+
id,
|
|
1701
|
+
appSlug
|
|
1702
|
+
}) {
|
|
1703
|
+
const app_slug = appSlug;
|
|
1704
|
+
const res = {
|
|
1705
|
+
method: "POST",
|
|
1706
|
+
url: `/${app_slug}/v1/me/mfa/factors/${id}/verify`
|
|
1707
|
+
};
|
|
1708
|
+
return res;
|
|
1709
|
+
}
|
|
1710
|
+
async function consumerMfaControllerConfirmEnrollment({
|
|
1711
|
+
id,
|
|
1712
|
+
appSlug,
|
|
1713
|
+
data
|
|
1714
|
+
}, config = {}) {
|
|
1715
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1716
|
+
const requestData = data;
|
|
1717
|
+
const res = await request({
|
|
1718
|
+
method: "POST",
|
|
1719
|
+
url: getConsumerMfaControllerConfirmEnrollmentUrl({
|
|
1720
|
+
id,
|
|
1721
|
+
appSlug
|
|
1722
|
+
}).url.toString(),
|
|
1723
|
+
data: requestData,
|
|
1724
|
+
...requestConfig
|
|
1725
|
+
});
|
|
1726
|
+
return res.data;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/_generated/clients/consumerMfa/consumerMfaControllerStepUpWebauthnOptions.ts
|
|
1730
|
+
function getConsumerMfaControllerStepUpWebauthnOptionsUrl({
|
|
1731
|
+
appSlug
|
|
1732
|
+
}) {
|
|
1733
|
+
const app_slug = appSlug;
|
|
1734
|
+
const res = {
|
|
1735
|
+
method: "POST",
|
|
1736
|
+
url: `/${app_slug}/v1/me/mfa/step-up/webauthn/options`
|
|
1737
|
+
};
|
|
1738
|
+
return res;
|
|
1739
|
+
}
|
|
1740
|
+
async function consumerMfaControllerStepUpWebauthnOptions({
|
|
1741
|
+
appSlug
|
|
1742
|
+
}, config = {}) {
|
|
1743
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1744
|
+
const res = await request({
|
|
1745
|
+
method: "POST",
|
|
1746
|
+
url: getConsumerMfaControllerStepUpWebauthnOptionsUrl({
|
|
1747
|
+
appSlug
|
|
1748
|
+
}).url.toString(),
|
|
1749
|
+
...requestConfig
|
|
1750
|
+
});
|
|
1751
|
+
return res.data;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/_generated/clients/consumerMfa/consumerMfaControllerStepUp.ts
|
|
1755
|
+
function getConsumerMfaControllerStepUpUrl({
|
|
1756
|
+
appSlug
|
|
1757
|
+
}) {
|
|
1758
|
+
const app_slug = appSlug;
|
|
1759
|
+
const res = {
|
|
1760
|
+
method: "POST",
|
|
1761
|
+
url: `/${app_slug}/v1/me/mfa/step-up`
|
|
1762
|
+
};
|
|
1763
|
+
return res;
|
|
1764
|
+
}
|
|
1765
|
+
async function consumerMfaControllerStepUp({
|
|
1766
|
+
appSlug,
|
|
1767
|
+
data
|
|
1768
|
+
}, config = {}) {
|
|
1769
|
+
const { client: request = client, ...requestConfig } = config;
|
|
1770
|
+
const requestData = data;
|
|
1771
|
+
const res = await request({
|
|
1772
|
+
method: "POST",
|
|
1773
|
+
url: getConsumerMfaControllerStepUpUrl({ appSlug }).url.toString(),
|
|
1774
|
+
data: requestData,
|
|
1775
|
+
...requestConfig
|
|
1776
|
+
});
|
|
1777
|
+
return res.data;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1560
1780
|
// src/_generated/clients/consumerVerify/consumerVerifyControllerVerify.ts
|
|
1561
1781
|
function getConsumerVerifyControllerVerifyUrl({
|
|
1562
1782
|
appSlug
|
|
@@ -1662,6 +1882,165 @@ async function consumerOAuthControllerClientCredentials({
|
|
|
1662
1882
|
return res.data;
|
|
1663
1883
|
}
|
|
1664
1884
|
|
|
1885
|
+
// src/passkeys.ts
|
|
1886
|
+
function toB64u(buf) {
|
|
1887
|
+
const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1888
|
+
let bin = "";
|
|
1889
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
1890
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1891
|
+
}
|
|
1892
|
+
function fromB64u(s) {
|
|
1893
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1894
|
+
const bin = atob(b64.padEnd(Math.ceil(b64.length / 4) * 4, "="));
|
|
1895
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
1896
|
+
}
|
|
1897
|
+
var snakeToCamel = (k) => k.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
1898
|
+
function camelizeKeys(value) {
|
|
1899
|
+
if (Array.isArray(value)) return value.map(camelizeKeys);
|
|
1900
|
+
if (value && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
1901
|
+
const out = {};
|
|
1902
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1903
|
+
out[snakeToCamel(k)] = camelizeKeys(v);
|
|
1904
|
+
}
|
|
1905
|
+
return out;
|
|
1906
|
+
}
|
|
1907
|
+
return value;
|
|
1908
|
+
}
|
|
1909
|
+
function decodeOptions(publicKey) {
|
|
1910
|
+
const pk = camelizeKeys(publicKey);
|
|
1911
|
+
const out = {
|
|
1912
|
+
...pk,
|
|
1913
|
+
challenge: fromB64u(pk.challenge)
|
|
1914
|
+
};
|
|
1915
|
+
if (pk.user) {
|
|
1916
|
+
out.user = { ...pk.user, id: fromB64u(pk.user.id) };
|
|
1917
|
+
}
|
|
1918
|
+
for (const list of ["excludeCredentials", "allowCredentials"]) {
|
|
1919
|
+
const entries = pk[list];
|
|
1920
|
+
if (entries) {
|
|
1921
|
+
out[list] = entries.map((c) => ({ ...c, id: fromB64u(c.id) }));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return out;
|
|
1925
|
+
}
|
|
1926
|
+
function encodeCredential(cred) {
|
|
1927
|
+
const r = cred.response;
|
|
1928
|
+
const response = {
|
|
1929
|
+
clientDataJSON: toB64u(r.clientDataJSON)
|
|
1930
|
+
};
|
|
1931
|
+
if (r.attestationObject) response.attestationObject = toB64u(r.attestationObject);
|
|
1932
|
+
if (r.authenticatorData) response.authenticatorData = toB64u(r.authenticatorData);
|
|
1933
|
+
if (r.signature) response.signature = toB64u(r.signature);
|
|
1934
|
+
if (r.userHandle) response.userHandle = toB64u(r.userHandle);
|
|
1935
|
+
return { id: cred.id, response };
|
|
1936
|
+
}
|
|
1937
|
+
var PasskeyError = class extends Error {
|
|
1938
|
+
code;
|
|
1939
|
+
cause;
|
|
1940
|
+
constructor(code, message, cause) {
|
|
1941
|
+
super(message);
|
|
1942
|
+
this.name = "PasskeyError";
|
|
1943
|
+
this.code = code;
|
|
1944
|
+
this.cause = cause;
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
function browserCredentials() {
|
|
1948
|
+
const nav = globalThis.navigator;
|
|
1949
|
+
const creds = nav?.credentials;
|
|
1950
|
+
if (!creds || typeof creds.create !== "function" || typeof creds.get !== "function") {
|
|
1951
|
+
return void 0;
|
|
1952
|
+
}
|
|
1953
|
+
return creds;
|
|
1954
|
+
}
|
|
1955
|
+
function isPasskeySupported() {
|
|
1956
|
+
const g = globalThis;
|
|
1957
|
+
if (typeof g.window === "undefined") return false;
|
|
1958
|
+
if (typeof g.PublicKeyCredential !== "function") return false;
|
|
1959
|
+
if (g.isSecureContext === false) return false;
|
|
1960
|
+
return browserCredentials() !== void 0;
|
|
1961
|
+
}
|
|
1962
|
+
function mapDomError(err) {
|
|
1963
|
+
const name = err?.name;
|
|
1964
|
+
const message = err?.message ?? String(err);
|
|
1965
|
+
switch (name) {
|
|
1966
|
+
case "NotAllowedError":
|
|
1967
|
+
return new PasskeyError(
|
|
1968
|
+
"cancelled",
|
|
1969
|
+
`Passkey prompt was cancelled, timed out, or no passkey matched: ${message}`,
|
|
1970
|
+
err
|
|
1971
|
+
);
|
|
1972
|
+
case "InvalidStateError":
|
|
1973
|
+
return new PasskeyError("invalid_state", `Authenticator refused: ${message}`, err);
|
|
1974
|
+
case "SecurityError":
|
|
1975
|
+
return new PasskeyError(
|
|
1976
|
+
"security",
|
|
1977
|
+
`Page origin does not match the app's rp_id: ${message}`,
|
|
1978
|
+
err
|
|
1979
|
+
);
|
|
1980
|
+
case "AbortError":
|
|
1981
|
+
return new PasskeyError("cancelled", `Passkey ceremony aborted: ${message}`, err);
|
|
1982
|
+
case "TimeoutError":
|
|
1983
|
+
return new PasskeyError(
|
|
1984
|
+
"timeout",
|
|
1985
|
+
`Passkey prompt timed out before a credential was produced: ${message}`,
|
|
1986
|
+
err
|
|
1987
|
+
);
|
|
1988
|
+
default:
|
|
1989
|
+
return new PasskeyError("unknown", `navigator.credentials failed: ${message}`, err);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function resolveCredentials(opts = {}) {
|
|
1993
|
+
const creds = opts.credentials ?? browserCredentials();
|
|
1994
|
+
if (!creds) {
|
|
1995
|
+
throw new PasskeyError(
|
|
1996
|
+
"unsupported",
|
|
1997
|
+
"navigator.credentials is not available \u2014 passkeys need a browser with WebAuthn (check isPasskeySupported())"
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
return creds;
|
|
2001
|
+
}
|
|
2002
|
+
function isCredentialLike(value) {
|
|
2003
|
+
if (!value || typeof value !== "object") return false;
|
|
2004
|
+
const v = value;
|
|
2005
|
+
return typeof v.id === "string" && !!v.response && typeof v.response === "object";
|
|
2006
|
+
}
|
|
2007
|
+
async function createPasskeyCredential(publicKey, opts = {}) {
|
|
2008
|
+
const creds = resolveCredentials(opts);
|
|
2009
|
+
let cred;
|
|
2010
|
+
try {
|
|
2011
|
+
cred = await creds.create({ publicKey: decodeOptions(publicKey), signal: opts.signal });
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
throw mapDomError(err);
|
|
2014
|
+
}
|
|
2015
|
+
if (!isCredentialLike(cred)) {
|
|
2016
|
+
throw new PasskeyError(
|
|
2017
|
+
"no_credential",
|
|
2018
|
+
"navigator.credentials.create() returned no usable credential"
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
return encodeCredential(cred);
|
|
2022
|
+
}
|
|
2023
|
+
async function getPasskeyCredential(publicKey, opts = {}) {
|
|
2024
|
+
const creds = resolveCredentials(opts);
|
|
2025
|
+
let cred;
|
|
2026
|
+
try {
|
|
2027
|
+
cred = await creds.get({
|
|
2028
|
+
publicKey: decodeOptions(publicKey),
|
|
2029
|
+
mediation: opts.mediation,
|
|
2030
|
+
signal: opts.signal
|
|
2031
|
+
});
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
throw mapDomError(err);
|
|
2034
|
+
}
|
|
2035
|
+
if (!isCredentialLike(cred)) {
|
|
2036
|
+
throw new PasskeyError(
|
|
2037
|
+
"no_credential",
|
|
2038
|
+
"navigator.credentials.get() returned no usable credential"
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
return encodeCredential(cred);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1665
2044
|
// src/jwt/errors.ts
|
|
1666
2045
|
var JwtVerifyError = class extends Error {
|
|
1667
2046
|
code;
|
|
@@ -2116,6 +2495,141 @@ var ConsumerScope = class {
|
|
|
2116
2495
|
)
|
|
2117
2496
|
};
|
|
2118
2497
|
// ─────────────────────────────────────────────────────────────
|
|
2498
|
+
// Passkeys — WebAuthn ceremonies, one call each. BROWSER ONLY at
|
|
2499
|
+
// call time (they drive navigator.credentials); importing is safe
|
|
2500
|
+
// anywhere. Base64url in both directions is handled here; the
|
|
2501
|
+
// wire stays snake_case except the spec-fixed response members
|
|
2502
|
+
// (clientDataJSON, attestationObject, authenticatorData, signature,
|
|
2503
|
+
// userHandle), which travel verbatim.
|
|
2504
|
+
// ─────────────────────────────────────────────────────────────
|
|
2505
|
+
passkeys = {
|
|
2506
|
+
/**
|
|
2507
|
+
* Enrol a passkey as an MFA factor for the signed-in end-user.
|
|
2508
|
+
* Requires the scope to carry the user's access token
|
|
2509
|
+
* (`new Auth({ auth: { type: "bearer", token } })`).
|
|
2510
|
+
*
|
|
2511
|
+
* `POST /me/mfa/factors { type: "webauthn" }` →
|
|
2512
|
+
* `navigator.credentials.create()` →
|
|
2513
|
+
* `POST /me/mfa/factors/:id/verify { credential }`.
|
|
2514
|
+
*
|
|
2515
|
+
* Resolves with the now-enabled factor and the user's recovery
|
|
2516
|
+
* codes — show those once, they are not retrievable later. The
|
|
2517
|
+
* factor stays disabled until the verify call succeeds, so an
|
|
2518
|
+
* abandoned prompt leaves nothing usable behind.
|
|
2519
|
+
*/
|
|
2520
|
+
enroll: async (opts = {}) => {
|
|
2521
|
+
const credentials = resolveCredentials(opts);
|
|
2522
|
+
const start = await consumerMfaControllerStartEnrollment(
|
|
2523
|
+
{
|
|
2524
|
+
appSlug: this.appSlug,
|
|
2525
|
+
data: { type: "webauthn", ...opts.label ? { label: opts.label } : {} }
|
|
2526
|
+
},
|
|
2527
|
+
{ client: this.client }
|
|
2528
|
+
);
|
|
2529
|
+
const publicKey = start.enrollment.public_key;
|
|
2530
|
+
const credential = await createPasskeyCredential(publicKey, { ...opts, credentials });
|
|
2531
|
+
return await consumerMfaControllerConfirmEnrollment(
|
|
2532
|
+
{ appSlug: this.appSlug, id: start.factor.id, data: { credential } },
|
|
2533
|
+
{ client: this.client }
|
|
2534
|
+
);
|
|
2535
|
+
},
|
|
2536
|
+
/**
|
|
2537
|
+
* Passwordless sign-in with a discoverable passkey. No password,
|
|
2538
|
+
* no email address: the authenticator finds its own passkey for
|
|
2539
|
+
* the app's rp_id.
|
|
2540
|
+
*
|
|
2541
|
+
* `POST /auth/passkey/options {}` → `navigator.credentials.get()`
|
|
2542
|
+
* → `POST /auth/passkey/verify { credential }`.
|
|
2543
|
+
*
|
|
2544
|
+
* Resolves with the session (`access_token`, `refresh_token`,
|
|
2545
|
+
* …; `amr: ["webauthn"]`). Rejects with `PasskeyError`
|
|
2546
|
+
* (`code: "cancelled"`) when the user dismisses the prompt or no
|
|
2547
|
+
* passkey matches, and with `AuthHttpError` 401 for a forged,
|
|
2548
|
+
* unknown, replayed or expired assertion — the server answers
|
|
2549
|
+
* those uniformly on purpose.
|
|
2550
|
+
*
|
|
2551
|
+
* The options call deliberately takes no identifier (it would be
|
|
2552
|
+
* an account-existence oracle); `tenant_id` is the only
|
|
2553
|
+
* accepted body member and only matters for multi-tenant apps.
|
|
2554
|
+
* Pass `mediation: "conditional"` for autofill-style UI.
|
|
2555
|
+
*/
|
|
2556
|
+
signIn: async (opts = {}) => {
|
|
2557
|
+
const credentials = resolveCredentials(opts);
|
|
2558
|
+
const { public_key } = await consumerAuthControllerPasskeyOptions(
|
|
2559
|
+
{
|
|
2560
|
+
appSlug: this.appSlug,
|
|
2561
|
+
data: opts.tenant_id ? { tenant_id: opts.tenant_id } : {}
|
|
2562
|
+
},
|
|
2563
|
+
{ client: this.client }
|
|
2564
|
+
);
|
|
2565
|
+
const credential = await getPasskeyCredential(
|
|
2566
|
+
public_key,
|
|
2567
|
+
{ ...opts, credentials }
|
|
2568
|
+
);
|
|
2569
|
+
return await consumerAuthControllerPasskeyVerify(
|
|
2570
|
+
{ appSlug: this.appSlug, data: { credential } },
|
|
2571
|
+
{ client: this.client }
|
|
2572
|
+
);
|
|
2573
|
+
},
|
|
2574
|
+
/**
|
|
2575
|
+
* Satisfy the MFA step of a password sign-in with a passkey.
|
|
2576
|
+
* Call it when `auth.signin()` answered
|
|
2577
|
+
* `{ mfa_required: true, mfa_token, factors }` and `factors`
|
|
2578
|
+
* contains a `webauthn` entry.
|
|
2579
|
+
*
|
|
2580
|
+
* `POST /auth/mfa/webauthn/options { mfa_token }` →
|
|
2581
|
+
* `navigator.credentials.get()` →
|
|
2582
|
+
* `POST /auth/mfa/verify { mfa_token, credential }`.
|
|
2583
|
+
*
|
|
2584
|
+
* Resolves with the session (`amr: ["pwd", "webauthn"]`, fresh
|
|
2585
|
+
* `mfa_at`). Possession of the `mfa_token` — a correct password
|
|
2586
|
+
* — is the authority for the options call.
|
|
2587
|
+
*/
|
|
2588
|
+
completeMfaChallenge: async (args) => {
|
|
2589
|
+
const { mfa_token, ...browserOpts } = args;
|
|
2590
|
+
const credentials = resolveCredentials(browserOpts);
|
|
2591
|
+
const { public_key } = await consumerAuthControllerMfaWebauthnOptions(
|
|
2592
|
+
{ appSlug: this.appSlug, data: { mfa_token } },
|
|
2593
|
+
{ client: this.client }
|
|
2594
|
+
);
|
|
2595
|
+
const credential = await getPasskeyCredential(
|
|
2596
|
+
public_key,
|
|
2597
|
+
{ ...browserOpts, credentials }
|
|
2598
|
+
);
|
|
2599
|
+
return await consumerAuthControllerMfaVerify(
|
|
2600
|
+
{ appSlug: this.appSlug, data: { mfa_token, credential } },
|
|
2601
|
+
{ client: this.client }
|
|
2602
|
+
);
|
|
2603
|
+
},
|
|
2604
|
+
/**
|
|
2605
|
+
* Re-confirm the signed-in user's identity inside a live session
|
|
2606
|
+
* (before a sensitive action). Requires the scope to carry the
|
|
2607
|
+
* user's access token.
|
|
2608
|
+
*
|
|
2609
|
+
* `POST /me/mfa/step-up/webauthn/options` →
|
|
2610
|
+
* `navigator.credentials.get()` →
|
|
2611
|
+
* `POST /me/mfa/step-up { credential }`.
|
|
2612
|
+
*
|
|
2613
|
+
* Resolves with `{ amr, mfa_at }`. A passkey assertion is never
|
|
2614
|
+
* tried as a recovery code, so a failed step-up never burns one.
|
|
2615
|
+
*/
|
|
2616
|
+
stepUp: async (opts = {}) => {
|
|
2617
|
+
const credentials = resolveCredentials(opts);
|
|
2618
|
+
const { public_key } = await consumerMfaControllerStepUpWebauthnOptions(
|
|
2619
|
+
{ appSlug: this.appSlug },
|
|
2620
|
+
{ client: this.client }
|
|
2621
|
+
);
|
|
2622
|
+
const credential = await getPasskeyCredential(
|
|
2623
|
+
public_key,
|
|
2624
|
+
{ ...opts, credentials }
|
|
2625
|
+
);
|
|
2626
|
+
return await consumerMfaControllerStepUp(
|
|
2627
|
+
{ appSlug: this.appSlug, data: { credential } },
|
|
2628
|
+
{ client: this.client }
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
// ─────────────────────────────────────────────────────────────
|
|
2119
2633
|
// OAuth M2M — client_credentials grant for service-to-service tokens
|
|
2120
2634
|
// ─────────────────────────────────────────────────────────────
|
|
2121
2635
|
oauth = {
|
|
@@ -2134,9 +2648,9 @@ function getAppControllerListMyAppsUrl() {
|
|
|
2134
2648
|
async function appControllerListMyApps({ params } = {}, config = {}) {
|
|
2135
2649
|
const { client: request = client, ...requestConfig } = config;
|
|
2136
2650
|
const mappedParams = params ? {
|
|
2651
|
+
workspace_id: params.workspaceId,
|
|
2137
2652
|
limit: params.limit,
|
|
2138
|
-
cursor: params.cursor
|
|
2139
|
-
workspace_id: params.workspaceId
|
|
2653
|
+
cursor: params.cursor
|
|
2140
2654
|
} : void 0;
|
|
2141
2655
|
const res = await request({
|
|
2142
2656
|
method: "GET",
|
|
@@ -2273,6 +2787,14 @@ exports.JwtInvalidError = JwtInvalidError;
|
|
|
2273
2787
|
exports.JwtIssuerMismatchError = JwtIssuerMismatchError;
|
|
2274
2788
|
exports.JwtNotYetValidError = JwtNotYetValidError;
|
|
2275
2789
|
exports.JwtVerifyError = JwtVerifyError;
|
|
2790
|
+
exports.PasskeyError = PasskeyError;
|
|
2791
|
+
exports.createPasskeyCredential = createPasskeyCredential;
|
|
2792
|
+
exports.decodeOptions = decodeOptions;
|
|
2793
|
+
exports.encodeCredential = encodeCredential;
|
|
2794
|
+
exports.fromB64u = fromB64u;
|
|
2795
|
+
exports.getPasskeyCredential = getPasskeyCredential;
|
|
2796
|
+
exports.isPasskeySupported = isPasskeySupported;
|
|
2797
|
+
exports.toB64u = toB64u;
|
|
2276
2798
|
exports.verifyAuthToken = verifyAuthToken;
|
|
2277
2799
|
//# sourceMappingURL=index.cjs.map
|
|
2278
2800
|
//# sourceMappingURL=index.cjs.map
|