@k9o/gate-react-native 1.0.0 → 1.1.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.
- package/README.md +176 -37
- package/package.json +1 -1
- package/src/GateAuthClient.ts +1 -1
- package/src/GateClient.ts +1 -1
- package/src/GateError.ts +2 -2
- package/src/Models.ts +3 -3
package/README.md
CHANGED
|
@@ -1,77 +1,216 @@
|
|
|
1
1
|
# @k9o/gate-react-native
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
Kilimanjaro Gate for React Native. Sign end users in with email and password and refresh their session.
|
|
4
|
+
The refresh token stays in the device keychain.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Install](#install)
|
|
9
|
+
- [Quick start](#quick-start)
|
|
10
|
+
- [Refresh and sign out](#refresh-and-sign-out)
|
|
11
|
+
- [Token storage](#token-storage)
|
|
12
|
+
- [Custom claims](#custom-claims)
|
|
13
|
+
- [Handle errors](#handle-errors)
|
|
14
|
+
- [Appendix](#appendix)
|
|
15
|
+
- [Requirements](#requirements)
|
|
16
|
+
- [Client options](#client-options)
|
|
17
|
+
- [Methods](#methods)
|
|
18
|
+
- [Error codes](#error-codes)
|
|
19
|
+
- [Related packages](#related-packages)
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
10
24
|
npm install @k9o/gate-react-native react-native-keychain
|
|
11
|
-
# or
|
|
12
|
-
yarn add @k9o/gate-react-native react-native-keychain
|
|
13
25
|
```
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
Yarn works too: `yarn add @k9o/gate-react-native react-native-keychain`. See [requirements](#requirements) for versions.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
The examples use a `tasks` app whose users sign in to the `member-portal` project. See [client options](#client-options) for the constructor.
|
|
16
32
|
|
|
17
33
|
```ts
|
|
18
|
-
import { GateClient } from
|
|
34
|
+
import { GateClient } from "@k9o/gate-react-native";
|
|
19
35
|
|
|
20
36
|
const gate = new GateClient({
|
|
21
|
-
orgBaseUrl:
|
|
22
|
-
projectSlug:
|
|
37
|
+
orgBaseUrl: "https://acme.api.k9o.io",
|
|
38
|
+
projectSlug: "member-portal",
|
|
23
39
|
});
|
|
40
|
+
```
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
Sign in. The refresh token is saved to the keychain for you.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const result = await gate.signIn({ email: "user@example.com", password: "correct-horse" });
|
|
46
|
+
|
|
47
|
+
console.log(result.user.email);
|
|
28
48
|
console.log(result.session.expiresAt);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
user@example.com
|
|
53
|
+
2026-01-15T10:30:00.000Z
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The result has a `user` and a `session`. See [methods](#methods).
|
|
57
|
+
|
|
58
|
+
## Refresh and sign out
|
|
59
|
+
|
|
60
|
+
With no arguments, `refreshToken` reads the stored token and saves the new one.
|
|
29
61
|
|
|
30
|
-
|
|
62
|
+
```ts
|
|
31
63
|
const refreshed = await gate.refreshToken();
|
|
32
64
|
|
|
33
|
-
|
|
65
|
+
console.log(refreshed.session.expiresAt);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
2026-01-15T11:30:00.000Z
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`signOut` ends the session on the server and clears the keychain. It does nothing if no token is stored.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
34
75
|
await gate.signOut();
|
|
35
76
|
```
|
|
36
77
|
|
|
37
|
-
##
|
|
78
|
+
## Token storage
|
|
79
|
+
|
|
80
|
+
By default the refresh token lives in the iOS Keychain or Android Keystore, through `react-native-keychain`.
|
|
38
81
|
|
|
39
|
-
|
|
82
|
+
To store it elsewhere, pass a `TokenStore`. This one keeps it in memory, which suits tests.
|
|
40
83
|
|
|
41
84
|
```ts
|
|
42
|
-
import
|
|
85
|
+
import { GateClient } from "@k9o/gate-react-native";
|
|
86
|
+
import type { TokenStore } from "@k9o/gate-react-native";
|
|
87
|
+
|
|
88
|
+
class MemoryTokenStore implements TokenStore {
|
|
89
|
+
private token: string | null = null;
|
|
90
|
+
|
|
91
|
+
async save(token: string): Promise<void> {
|
|
92
|
+
this.token = token;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async load(): Promise<string | null> {
|
|
96
|
+
return this.token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async delete(): Promise<void> {
|
|
100
|
+
this.token = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
43
103
|
|
|
44
|
-
class MyStore implements TokenStore { ... }
|
|
45
104
|
const gate = new GateClient({
|
|
46
|
-
orgBaseUrl:
|
|
47
|
-
projectSlug:
|
|
48
|
-
tokenStore: new
|
|
105
|
+
orgBaseUrl: "https://acme.api.k9o.io",
|
|
106
|
+
projectSlug: "member-portal",
|
|
107
|
+
tokenStore: new MemoryTokenStore(),
|
|
49
108
|
});
|
|
50
109
|
```
|
|
51
110
|
|
|
52
111
|
## Custom claims
|
|
53
112
|
|
|
113
|
+
Claims added by extensions appear on the access token under their namespace. Decoding does not verify the signature.
|
|
114
|
+
|
|
54
115
|
```ts
|
|
55
|
-
import { AccessToken } from
|
|
116
|
+
import { AccessToken } from "@k9o/gate-react-native";
|
|
56
117
|
|
|
57
118
|
const token = AccessToken.decode(result.session.accessToken);
|
|
58
|
-
|
|
119
|
+
|
|
120
|
+
if (token !== null) {
|
|
121
|
+
const claims = token.customClaims;
|
|
122
|
+
const partners = claims["partners"];
|
|
123
|
+
|
|
124
|
+
if (partners !== undefined) {
|
|
125
|
+
const role = partners["partner_role"];
|
|
126
|
+
console.log(role);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
extension_org_admin
|
|
59
133
|
```
|
|
60
134
|
|
|
61
|
-
|
|
135
|
+
`token` also has `subject`, `issuer`, `issuedAt`, `expiresAt` and `allClaims`.
|
|
136
|
+
|
|
137
|
+
## Handle errors
|
|
138
|
+
|
|
139
|
+
Every failure is a `GateError`. Check `code`.
|
|
62
140
|
|
|
63
141
|
```ts
|
|
64
|
-
import { GateError } from
|
|
142
|
+
import { GateError } from "@k9o/gate-react-native";
|
|
65
143
|
|
|
66
144
|
try {
|
|
67
|
-
await gate.signIn({ email, password });
|
|
68
|
-
} catch (
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
case 'INVALID_CREDENTIALS': break;
|
|
72
|
-
case 'NETWORK': break;
|
|
73
|
-
case 'UNKNOWN': console.log(e.statusCode, e.body); break;
|
|
74
|
-
}
|
|
145
|
+
await gate.signIn({ email: "user@example.com", password: "wrong" });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof GateError && error.code === "INVALID_CREDENTIALS") {
|
|
148
|
+
console.log(error.message);
|
|
75
149
|
}
|
|
76
150
|
}
|
|
77
151
|
```
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
Invalid credentials.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
See [error codes](#error-codes) for all of them.
|
|
158
|
+
|
|
159
|
+
## Appendix
|
|
160
|
+
|
|
161
|
+
### Requirements
|
|
162
|
+
|
|
163
|
+
| Requirement | Version |
|
|
164
|
+
|---|---|
|
|
165
|
+
| React | 18 or later |
|
|
166
|
+
| React Native | 0.74 or later |
|
|
167
|
+
| `react-native-keychain` | 8 or later (follow its native setup) |
|
|
168
|
+
| TypeScript (optional) | 5.3 or later |
|
|
169
|
+
|
|
170
|
+
### Client options
|
|
171
|
+
|
|
172
|
+
`new GateClient({ orgBaseUrl, projectSlug, tokenStore? })`
|
|
173
|
+
|
|
174
|
+
| Option | Required | Meaning |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| `orgBaseUrl` | Yes | Your organization's API host, such as `https://acme.api.k9o.io` |
|
|
177
|
+
| `projectSlug` | Yes | The project users sign in to |
|
|
178
|
+
| `tokenStore` | No | Where the refresh token is kept; the keychain by default |
|
|
179
|
+
|
|
180
|
+
### Methods
|
|
181
|
+
|
|
182
|
+
| Method | What it does |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `signIn({ email, password })` | Sign in; resolves to `{ user, session }` |
|
|
185
|
+
| `refreshToken({ refreshToken?, projectHint? })` | Rotate the session; uses the stored token when none is passed |
|
|
186
|
+
| `signOut({ refreshToken? })` | End the session and clear the stored token |
|
|
187
|
+
| `AccessToken.decode(jwt)` | Read an access token's claims; `null` if it is not a JWT |
|
|
188
|
+
|
|
189
|
+
| Result field | Contents |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `user` | `id`, `email`, `status`, `roles`, `groups`, timestamps |
|
|
192
|
+
| `session` | `accessToken`, `refreshToken`, `expiresAt` |
|
|
193
|
+
|
|
194
|
+
### Error codes
|
|
195
|
+
|
|
196
|
+
| Code | When |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `INVALID_CREDENTIALS` | Wrong email or password |
|
|
199
|
+
| `UNAUTHENTICATED` | The session expired, or no refresh token is stored |
|
|
200
|
+
| `FORBIDDEN` | The session is not allowed to do this |
|
|
201
|
+
| `NETWORK` | The request could not reach the server |
|
|
202
|
+
| `DECODING` | The response was not in the expected shape |
|
|
203
|
+
| `UNKNOWN` | Any other response; `statusCode` and `body` are set |
|
|
204
|
+
|
|
205
|
+
### Related packages
|
|
206
|
+
|
|
207
|
+
| Package | Purpose |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `@k9o/kibo-react-native` | Client-side encryption with your own KMS |
|
|
210
|
+
| `@k9o/core` | The TypeScript SDK for the rest of the platform |
|
|
211
|
+
|
|
212
|
+
Documentation: <https://kilimanjaro.dev>
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
Apache-2.0 © Formelo Inc. See `LICENSE` and `NOTICE`.
|
package/package.json
CHANGED
package/src/GateAuthClient.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { parseEnvelope } from "./Models";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Internal fetch-based transport layer for @k9o/gate-react-native.
|
|
6
|
-
* Constructs URLs using the
|
|
6
|
+
* Constructs URLs using the `/@<projectSlug>/gate/<suffix>` grammar.
|
|
7
7
|
*/
|
|
8
8
|
export class GateAuthClient {
|
|
9
9
|
private readonly baseUrl: string;
|
package/src/GateClient.ts
CHANGED
|
@@ -63,7 +63,7 @@ export class GateClient {
|
|
|
63
63
|
* If `refreshToken` is omitted, loads the token from the store.
|
|
64
64
|
* Updates the store with the new refresh token on success.
|
|
65
65
|
*
|
|
66
|
-
* `projectHint` is optional
|
|
66
|
+
* `projectHint` is optional; omit for most configurations.
|
|
67
67
|
*
|
|
68
68
|
* Mirrors: `POST /@<projectSlug>/gate/refresh`
|
|
69
69
|
*/
|
package/src/GateError.ts
CHANGED
|
@@ -67,9 +67,9 @@ export class GateError extends Error {
|
|
|
67
67
|
|
|
68
68
|
// MARK: - Wire error parsing
|
|
69
69
|
|
|
70
|
-
// The gateway emits a KError at the ROOT of the error body
|
|
70
|
+
// The gateway emits a KError at the ROOT of the error body:
|
|
71
71
|
// `{ code, message, context?, recommendation? }` — NOT wrapped in `{ error: … }`.
|
|
72
|
-
// Codes are platform-namespaced (`K9O_*`
|
|
72
|
+
// Codes are platform-namespaced (`K9O_*`), so the
|
|
73
73
|
// prefix is stripped before matching the SDK's typed enum.
|
|
74
74
|
interface WireKError {
|
|
75
75
|
code?: string;
|
package/src/Models.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface AuthResult {
|
|
|
29
29
|
session: Session;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Standard
|
|
32
|
+
/** Standard response envelope. */
|
|
33
33
|
interface Envelope<T> {
|
|
34
34
|
data: T;
|
|
35
35
|
meta?: Record<string, unknown>;
|
|
@@ -54,7 +54,7 @@ export function parseEnvelope<T>(body: string): T {
|
|
|
54
54
|
/**
|
|
55
55
|
* Lightweight access-token decoder. Decodes the JWT payload without
|
|
56
56
|
* signature verification. Exposes `ext.<namespace>.<name>` custom-claim
|
|
57
|
-
* flatten from
|
|
57
|
+
* flatten from.
|
|
58
58
|
*/
|
|
59
59
|
export class AccessToken {
|
|
60
60
|
readonly subject: string | undefined;
|
|
@@ -72,7 +72,7 @@ export class AccessToken {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Custom claims namespaced under `ext.<namespace>` per
|
|
75
|
+
* Custom claims namespaced under `ext.<namespace>` per.
|
|
76
76
|
* `customClaims['partners']?.['partner_role']` → "extension_org_admin"
|
|
77
77
|
*/
|
|
78
78
|
get customClaims(): Record<string, Record<string, unknown>> {
|