@swivy/expo 0.0.0-stage → 1.0.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 +19 -2
- package/dist/packages/expo/src/index.d.ts +270 -0
- package/dist/packages/expo/src/index.js +541 -0
- package/package.json +35 -4
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @swivy/expo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Swivy expo SDK. Testnet preview; production onboarding is gated on the Swop canary.
|
|
4
|
+
|
|
5
|
+
Public npm distribution is being prepared. Before first publication, use all SDK tarballs
|
|
6
|
+
from one operator-supplied release and verify its SHA256SUMS. After publication:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @swivy/expo@next
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Use an environment app ID and publishable key for browser/mobile apps. Keep environment
|
|
13
|
+
secret keys on the backend and deployment bootstrap credentials with the deployment operator.
|
|
14
|
+
Choose email or SMS in your environment policy; SMS recovery alone does not enable SMS login.
|
|
15
|
+
Never log login codes, runtime sessions or signing material.
|
|
16
|
+
|
|
17
|
+
Browser apps currently need a same-origin reverse proxy for `/v1/runtime/*`; the API
|
|
18
|
+
does not advertise cross-origin browser access. Backend and native clients can use the API directly.
|
|
19
|
+
|
|
20
|
+
The client exposes `requestSmsCode(phoneNumber)` and `verifySmsCode(challengeId, code)`. Supply phone numbers in E.164 format.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { Chain, CustodyMode, LinkedWalletDescriptor, ManagedAccountDescriptor, OfflineSigningMeshPacket, OfflineTransactionSigningEnvelope, OfflineTransactionSigningResponseEnvelope, TransactionIntent, TypedDataDescriptor, WalletPurpose } from "@swivy/core";
|
|
2
|
+
export interface SwivyExpoClientConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
appId?: string;
|
|
5
|
+
publishableKey?: string;
|
|
6
|
+
environmentId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Legacy alias for publishableKey while existing apps migrate.
|
|
9
|
+
*/
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
sessionToken?: string;
|
|
12
|
+
sessionPersistence?: SwivyExpoSessionPersistenceAdapter;
|
|
13
|
+
passkeyAdapter?: SwivyExpoPasskeyAdapter;
|
|
14
|
+
bluetoothMeshAdapter?: SwivyExpoBluetoothMeshAdapter;
|
|
15
|
+
}
|
|
16
|
+
export interface SwivyExpoSessionPersistenceAdapter {
|
|
17
|
+
load(): Promise<string | undefined>;
|
|
18
|
+
save(token: string): Promise<void>;
|
|
19
|
+
clear(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface SwivyExpoAsyncKeyValueStoreLike {
|
|
22
|
+
getItem(key: string): Promise<string | null>;
|
|
23
|
+
setItem(key: string, value: string): Promise<void>;
|
|
24
|
+
removeItem(key: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function createAsyncStorageSessionPersistence(storage: SwivyExpoAsyncKeyValueStoreLike, key?: string): SwivyExpoSessionPersistenceAdapter;
|
|
27
|
+
export type SwivyChallengePurpose = "login" | "account_export" | "intent_approval" | "recovery" | "session_delegation" | "wallet_reconstruction";
|
|
28
|
+
export interface SwivyExpoRuntimeAuthMethod {
|
|
29
|
+
id: string;
|
|
30
|
+
type: string;
|
|
31
|
+
label: string;
|
|
32
|
+
active: boolean;
|
|
33
|
+
recoveryCapable?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface SwivyExpoRuntimeSession {
|
|
36
|
+
sessionToken?: string;
|
|
37
|
+
managedAccounts?: ManagedAccountDescriptor[];
|
|
38
|
+
linkedWallets?: LinkedWalletDescriptor[];
|
|
39
|
+
authMethods?: SwivyExpoRuntimeAuthMethod[];
|
|
40
|
+
environmentPolicy?: {
|
|
41
|
+
allowedChains?: Chain[];
|
|
42
|
+
walletPolicy?: {
|
|
43
|
+
allowManagedAccounts?: boolean;
|
|
44
|
+
allowSplitCustody?: boolean;
|
|
45
|
+
autoCreateManagedAccounts?: boolean;
|
|
46
|
+
defaultCustodyMode?: CustodyMode;
|
|
47
|
+
managedAccountChains?: Chain[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface SwivyExpoListAccountsPayload {
|
|
53
|
+
managedAccounts: ManagedAccountDescriptor[];
|
|
54
|
+
linkedWallets: LinkedWalletDescriptor[];
|
|
55
|
+
}
|
|
56
|
+
export interface SwivyExpoManagedAccountCreateOptions {
|
|
57
|
+
label?: string;
|
|
58
|
+
purpose?: WalletPurpose;
|
|
59
|
+
custodyMode?: CustodyMode;
|
|
60
|
+
splitCustody?: {
|
|
61
|
+
deviceShare: {
|
|
62
|
+
commitment: string;
|
|
63
|
+
encryptedPayload?: string;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
};
|
|
66
|
+
recoveryShare?: {
|
|
67
|
+
commitment: string;
|
|
68
|
+
encryptedPayload?: string;
|
|
69
|
+
metadata?: Record<string, unknown>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface SwivyExpoManagedAccountResolution {
|
|
74
|
+
account: ManagedAccountDescriptor;
|
|
75
|
+
existing: boolean;
|
|
76
|
+
attestation?: unknown;
|
|
77
|
+
}
|
|
78
|
+
export interface SwivyExpoWaitForIntentOptions {
|
|
79
|
+
timeoutMs?: number;
|
|
80
|
+
pollIntervalMs?: number;
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
}
|
|
83
|
+
export interface SwivyExpoSigningPayload {
|
|
84
|
+
accountId: string;
|
|
85
|
+
chain: Chain;
|
|
86
|
+
challengeId?: string;
|
|
87
|
+
payload: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
export interface SwivyExpoTypedDataSigningPayload {
|
|
90
|
+
accountId: string;
|
|
91
|
+
challengeId?: string;
|
|
92
|
+
typedData: TypedDataDescriptor;
|
|
93
|
+
}
|
|
94
|
+
export interface SwivyExpoPasskeyRegistrationStartPayload {
|
|
95
|
+
authMethod: {
|
|
96
|
+
id: string;
|
|
97
|
+
type: "passkey";
|
|
98
|
+
label: string;
|
|
99
|
+
active: boolean;
|
|
100
|
+
recoveryCapable: boolean;
|
|
101
|
+
};
|
|
102
|
+
challengeId: string;
|
|
103
|
+
methodType: "passkey";
|
|
104
|
+
options: Record<string, unknown>;
|
|
105
|
+
expiresAt: string;
|
|
106
|
+
}
|
|
107
|
+
export interface SwivyExpoPasskeyChallengePayload {
|
|
108
|
+
challengeId: string;
|
|
109
|
+
purpose: SwivyChallengePurpose;
|
|
110
|
+
expiresAt: string;
|
|
111
|
+
methodType: "passkey";
|
|
112
|
+
options: Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
export interface SwivyExpoPasskeyEnsureResult {
|
|
115
|
+
authMethod: SwivyExpoRuntimeAuthMethod;
|
|
116
|
+
existing: boolean;
|
|
117
|
+
enrollment?: Awaited<ReturnType<SwivyExpoClient["enrollPasskey"]>>;
|
|
118
|
+
}
|
|
119
|
+
export interface SwivyExpoPasskeyEnrollmentContext {
|
|
120
|
+
authMethodId: string;
|
|
121
|
+
challengeId: string;
|
|
122
|
+
label: string;
|
|
123
|
+
}
|
|
124
|
+
export interface SwivyExpoPasskeyAuthenticationContext {
|
|
125
|
+
challengeId: string;
|
|
126
|
+
purpose: SwivyChallengePurpose;
|
|
127
|
+
authMethodId: string;
|
|
128
|
+
}
|
|
129
|
+
export interface SwivyExpoPasskeyAdapter {
|
|
130
|
+
isSupported?(): Promise<boolean>;
|
|
131
|
+
register(options: Record<string, unknown>, context: SwivyExpoPasskeyEnrollmentContext): Promise<Record<string, unknown>>;
|
|
132
|
+
authenticate(options: Record<string, unknown>, context: SwivyExpoPasskeyAuthenticationContext): Promise<Record<string, unknown>>;
|
|
133
|
+
}
|
|
134
|
+
export interface SwivyExpoBluetoothMeshPeer {
|
|
135
|
+
id: string;
|
|
136
|
+
label?: string;
|
|
137
|
+
rssi?: number;
|
|
138
|
+
metadata?: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
export interface SwivyExpoBluetoothMeshSendOptions {
|
|
141
|
+
ttlSeconds?: number;
|
|
142
|
+
retryCount?: number;
|
|
143
|
+
retryDelayMs?: number;
|
|
144
|
+
maxPayloadBytes?: number;
|
|
145
|
+
}
|
|
146
|
+
export interface SwivyExpoBluetoothMeshAdapter {
|
|
147
|
+
isSupported?(): Promise<boolean>;
|
|
148
|
+
start(params: {
|
|
149
|
+
serviceId: string;
|
|
150
|
+
onPeerDiscovered(peer: SwivyExpoBluetoothMeshPeer): void;
|
|
151
|
+
onPacket(packet: OfflineSigningMeshPacket, peer?: SwivyExpoBluetoothMeshPeer): void;
|
|
152
|
+
}): Promise<void>;
|
|
153
|
+
stop(): Promise<void>;
|
|
154
|
+
send(packet: OfflineSigningMeshPacket, peerId?: string): Promise<void>;
|
|
155
|
+
}
|
|
156
|
+
export interface SwivyExpoPasskeyApprovalResult {
|
|
157
|
+
challenge: SwivyExpoPasskeyChallengePayload;
|
|
158
|
+
verification: unknown;
|
|
159
|
+
authenticationResponse: Record<string, unknown>;
|
|
160
|
+
}
|
|
161
|
+
export interface SwivyExpoOidcDiscoveryDocument {
|
|
162
|
+
issuer: string;
|
|
163
|
+
authorization_endpoint: string;
|
|
164
|
+
token_endpoint: string;
|
|
165
|
+
userinfo_endpoint?: string;
|
|
166
|
+
jwks_uri: string;
|
|
167
|
+
end_session_endpoint?: string;
|
|
168
|
+
scopes_supported?: string[];
|
|
169
|
+
response_types_supported?: string[];
|
|
170
|
+
grant_types_supported?: string[];
|
|
171
|
+
code_challenge_methods_supported?: string[];
|
|
172
|
+
[key: string]: unknown;
|
|
173
|
+
}
|
|
174
|
+
export interface SwivyExpoBluetoothMeshSession {
|
|
175
|
+
stop(): Promise<void>;
|
|
176
|
+
sendEnvelope(envelope: OfflineTransactionSigningEnvelope | OfflineTransactionSigningResponseEnvelope | string, peerId?: string, options?: SwivyExpoBluetoothMeshSendOptions): Promise<{
|
|
177
|
+
messageId: string;
|
|
178
|
+
packetsSent: number;
|
|
179
|
+
}>;
|
|
180
|
+
}
|
|
181
|
+
export declare class SwivyExpoClient {
|
|
182
|
+
private readonly config;
|
|
183
|
+
private sessionToken?;
|
|
184
|
+
private initialized;
|
|
185
|
+
private readonly sessionTokenListeners;
|
|
186
|
+
constructor(config: SwivyExpoClientConfig);
|
|
187
|
+
static create(config: SwivyExpoClientConfig): Promise<SwivyExpoClient>;
|
|
188
|
+
initialize(): Promise<this>;
|
|
189
|
+
getSessionToken(): string | undefined;
|
|
190
|
+
setSessionToken(token: string | undefined): Promise<void>;
|
|
191
|
+
clearSessionToken(): Promise<void>;
|
|
192
|
+
onSessionTokenChange(listener: (token: string | undefined) => void): () => void;
|
|
193
|
+
requestEmailCode(email: string, externalUserId?: string): Promise<any>;
|
|
194
|
+
verifyEmailCode(email: string, code: string, externalUserId?: string): Promise<SwivyExpoRuntimeSession>;
|
|
195
|
+
exchangeOidcToken(token: string): Promise<SwivyExpoRuntimeSession>;
|
|
196
|
+
getMe(): Promise<SwivyExpoRuntimeSession>;
|
|
197
|
+
hydrateRuntimeSession(): Promise<SwivyExpoRuntimeSession>;
|
|
198
|
+
startPasskeyRegistration(label?: string): Promise<SwivyExpoPasskeyRegistrationStartPayload>;
|
|
199
|
+
finishPasskeyRegistration(authMethodId: string, challengeId: string, registrationResponse: Record<string, unknown>): Promise<any>;
|
|
200
|
+
/** Create a new account with a passkey only (Privy signupWithPasskey). */
|
|
201
|
+
signupWithPasskey(options?: {
|
|
202
|
+
displayName?: string;
|
|
203
|
+
}): Promise<any>;
|
|
204
|
+
/** Log in with any passkey registered for this app (Privy loginWithPasskey). */
|
|
205
|
+
loginWithPasskey(): Promise<any>;
|
|
206
|
+
/** Send a login code by SMS (Privy loginWithSMS, step 1). Phone in E.164. */
|
|
207
|
+
requestSmsCode(phoneNumber: string): Promise<any>;
|
|
208
|
+
verifySmsCode(challengeId: string, code: string): Promise<any>;
|
|
209
|
+
enrollPasskey(label?: string): Promise<any>;
|
|
210
|
+
ensurePasskey(label?: string): Promise<SwivyExpoPasskeyEnsureResult>;
|
|
211
|
+
createChallenge(authMethodId: string, purpose: SwivyChallengePurpose): Promise<any>;
|
|
212
|
+
verifyChallenge(challengeId: string, payload: Record<string, unknown>): Promise<any>;
|
|
213
|
+
approveWithPasskey(authMethodId: string, purpose: SwivyChallengePurpose): Promise<SwivyExpoPasskeyApprovalResult>;
|
|
214
|
+
listAccounts(): Promise<SwivyExpoListAccountsPayload>;
|
|
215
|
+
listEmbeddedWallets(): Promise<ManagedAccountDescriptor[]>;
|
|
216
|
+
getManagedAccount(chain: Chain, options?: {
|
|
217
|
+
purpose?: WalletPurpose;
|
|
218
|
+
}): Promise<ManagedAccountDescriptor | null>;
|
|
219
|
+
getEmbeddedWallet(chain: Chain): Promise<ManagedAccountDescriptor | null>;
|
|
220
|
+
createManagedAccount(chain: Chain, options?: string | SwivyExpoManagedAccountCreateOptions): Promise<any>;
|
|
221
|
+
ensureManagedAccount(chain: Chain, options?: string | SwivyExpoManagedAccountCreateOptions): Promise<SwivyExpoManagedAccountResolution>;
|
|
222
|
+
ensureEmbeddedWallet(chain: Chain, options?: string | SwivyExpoManagedAccountCreateOptions): Promise<SwivyExpoManagedAccountResolution>;
|
|
223
|
+
createSigningIntent(body: Record<string, unknown>): Promise<any>;
|
|
224
|
+
createTransactionIntent(body: Record<string, unknown>): Promise<any>;
|
|
225
|
+
getIntent(intentId: string): Promise<any>;
|
|
226
|
+
waitForIntent(intentId: string, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
227
|
+
createSigningIntentAndWait(body: Record<string, unknown>, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
228
|
+
createTransactionIntentAndWait(body: Record<string, unknown>, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
229
|
+
signMessageAndWait(body: SwivyExpoSigningPayload & {
|
|
230
|
+
payload: {
|
|
231
|
+
message?: string;
|
|
232
|
+
messageHex?: string;
|
|
233
|
+
messageBase64?: string;
|
|
234
|
+
};
|
|
235
|
+
}, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
236
|
+
signTypedDataAndWait(body: SwivyExpoTypedDataSigningPayload, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
237
|
+
signTransactionAndWait(body: SwivyExpoSigningPayload & {
|
|
238
|
+
payload: {
|
|
239
|
+
transaction?: Record<string, unknown>;
|
|
240
|
+
serializedTransactionBase64?: string;
|
|
241
|
+
};
|
|
242
|
+
}, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
243
|
+
createOfflineTransactionSigningEnvelope(body: {
|
|
244
|
+
accountId: string;
|
|
245
|
+
chain: Chain;
|
|
246
|
+
payload: {
|
|
247
|
+
network?: string | null;
|
|
248
|
+
transaction?: Record<string, unknown>;
|
|
249
|
+
serializedTransactionBase64?: string;
|
|
250
|
+
requestedValueUsd?: number | null;
|
|
251
|
+
};
|
|
252
|
+
expiresAt?: Date | string | null;
|
|
253
|
+
requester?: OfflineTransactionSigningEnvelope["requester"];
|
|
254
|
+
metadata?: Record<string, unknown>;
|
|
255
|
+
}): Promise<OfflineTransactionSigningEnvelope>;
|
|
256
|
+
encodeOfflineSigningEnvelope(envelope: OfflineTransactionSigningEnvelope | OfflineTransactionSigningResponseEnvelope): string;
|
|
257
|
+
decodeOfflineSigningEnvelope(envelope: string): OfflineTransactionSigningEnvelope | OfflineTransactionSigningResponseEnvelope;
|
|
258
|
+
startBluetoothMeshOfflineSigning(params: {
|
|
259
|
+
serviceId?: string;
|
|
260
|
+
onPeerDiscovered?(peer: SwivyExpoBluetoothMeshPeer): void;
|
|
261
|
+
onEnvelope(envelope: OfflineTransactionSigningEnvelope | OfflineTransactionSigningResponseEnvelope, peer?: SwivyExpoBluetoothMeshPeer): void | Promise<void>;
|
|
262
|
+
}): Promise<SwivyExpoBluetoothMeshSession>;
|
|
263
|
+
executeTransactionAndWait(body: Record<string, unknown>, options?: SwivyExpoWaitForIntentOptions): Promise<TransactionIntent>;
|
|
264
|
+
getOidcDiscovery(appId?: string | undefined): Promise<SwivyExpoOidcDiscoveryDocument>;
|
|
265
|
+
getOidcJwks(appId?: string | undefined): Promise<any>;
|
|
266
|
+
private requirePasskeyAdapter;
|
|
267
|
+
private requireBluetoothMeshAdapter;
|
|
268
|
+
private request;
|
|
269
|
+
private delay;
|
|
270
|
+
}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { createOfflineTransactionSigningEnvelope, createOfflineSigningMeshPackets, decodeOfflineSigningEnvelope, encodeOfflineSigningEnvelope, OfflineSigningMeshReassembler, } from "@swivy/core";
|
|
2
|
+
export function createAsyncStorageSessionPersistence(storage, key = "swivy:session-token") {
|
|
3
|
+
return {
|
|
4
|
+
async load() {
|
|
5
|
+
const value = await storage.getItem(key);
|
|
6
|
+
return value ?? undefined;
|
|
7
|
+
},
|
|
8
|
+
async save(token) {
|
|
9
|
+
await storage.setItem(key, token);
|
|
10
|
+
},
|
|
11
|
+
async clear() {
|
|
12
|
+
await storage.removeItem(key);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function parseJsonResponseBody(response, text) {
|
|
17
|
+
if (!text) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
25
|
+
if (response.ok) {
|
|
26
|
+
throw new Error(contentType ? `invalid_json_response:${contentType}` : "invalid_json_response");
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`http_${response.status}_non_json_response`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function resolveResponseErrorMessage(response, payload) {
|
|
32
|
+
if (typeof payload?.error === "string" && payload.error) {
|
|
33
|
+
return payload.error;
|
|
34
|
+
}
|
|
35
|
+
if (typeof payload?.error?.message === "string" && payload.error.message) {
|
|
36
|
+
return payload.error.message;
|
|
37
|
+
}
|
|
38
|
+
if (typeof payload?.message === "string" && payload.message) {
|
|
39
|
+
return payload.message;
|
|
40
|
+
}
|
|
41
|
+
return response.statusText || `http_${response.status}`;
|
|
42
|
+
}
|
|
43
|
+
export class SwivyExpoClient {
|
|
44
|
+
config;
|
|
45
|
+
sessionToken;
|
|
46
|
+
initialized = false;
|
|
47
|
+
sessionTokenListeners = new Set();
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
this.sessionToken = config.sessionToken;
|
|
51
|
+
}
|
|
52
|
+
static async create(config) {
|
|
53
|
+
const client = new SwivyExpoClient(config);
|
|
54
|
+
await client.initialize();
|
|
55
|
+
return client;
|
|
56
|
+
}
|
|
57
|
+
async initialize() {
|
|
58
|
+
if (this.initialized) {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
this.initialized = true;
|
|
62
|
+
if (!this.sessionToken && this.config.sessionPersistence) {
|
|
63
|
+
this.sessionToken = await this.config.sessionPersistence.load();
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
getSessionToken() {
|
|
68
|
+
return this.sessionToken;
|
|
69
|
+
}
|
|
70
|
+
async setSessionToken(token) {
|
|
71
|
+
this.sessionToken = token;
|
|
72
|
+
if (token) {
|
|
73
|
+
await this.config.sessionPersistence?.save(token);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await this.config.sessionPersistence?.clear();
|
|
77
|
+
}
|
|
78
|
+
for (const listener of this.sessionTokenListeners) {
|
|
79
|
+
listener(token);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async clearSessionToken() {
|
|
83
|
+
await this.setSessionToken(undefined);
|
|
84
|
+
}
|
|
85
|
+
onSessionTokenChange(listener) {
|
|
86
|
+
this.sessionTokenListeners.add(listener);
|
|
87
|
+
return () => {
|
|
88
|
+
this.sessionTokenListeners.delete(listener);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async requestEmailCode(email, externalUserId) {
|
|
92
|
+
await this.initialize();
|
|
93
|
+
return this.request("/v1/runtime/auth/email/request", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: { email, externalUserId },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async verifyEmailCode(email, code, externalUserId) {
|
|
99
|
+
await this.initialize();
|
|
100
|
+
const payload = await this.request("/v1/runtime/auth/email/verify", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: { email, code, externalUserId },
|
|
103
|
+
});
|
|
104
|
+
if (payload.sessionToken) {
|
|
105
|
+
await this.setSessionToken(payload.sessionToken);
|
|
106
|
+
}
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
109
|
+
async exchangeOidcToken(token) {
|
|
110
|
+
await this.initialize();
|
|
111
|
+
const payload = await this.request("/v1/runtime/auth/oidc/exchange", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
body: { token },
|
|
114
|
+
});
|
|
115
|
+
if (payload.sessionToken) {
|
|
116
|
+
await this.setSessionToken(payload.sessionToken);
|
|
117
|
+
}
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
async getMe() {
|
|
121
|
+
await this.initialize();
|
|
122
|
+
return this.request("/v1/runtime/me", { method: "GET", runtime: true });
|
|
123
|
+
}
|
|
124
|
+
async hydrateRuntimeSession() {
|
|
125
|
+
return this.getMe();
|
|
126
|
+
}
|
|
127
|
+
async startPasskeyRegistration(label = "Passkey") {
|
|
128
|
+
await this.initialize();
|
|
129
|
+
return this.request("/v1/runtime/mfa/passkeys/register/start", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: { label },
|
|
132
|
+
runtime: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async finishPasskeyRegistration(authMethodId, challengeId, registrationResponse) {
|
|
136
|
+
await this.initialize();
|
|
137
|
+
return this.request("/v1/runtime/mfa/passkeys/register/finish", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
body: { authMethodId, challengeId, registrationResponse },
|
|
140
|
+
runtime: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** Create a new account with a passkey only (Privy signupWithPasskey). */
|
|
144
|
+
async signupWithPasskey(options = {}) {
|
|
145
|
+
await this.initialize();
|
|
146
|
+
const adapter = await this.requirePasskeyAdapter();
|
|
147
|
+
const started = await this.request("/v1/runtime/auth/passkeys/signup/start", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
body: { displayName: options.displayName },
|
|
150
|
+
});
|
|
151
|
+
const registrationResponse = await adapter.register(started.options, {
|
|
152
|
+
authMethodId: "",
|
|
153
|
+
challengeId: started.challengeId,
|
|
154
|
+
label: "Passkey",
|
|
155
|
+
});
|
|
156
|
+
const payload = await this.request("/v1/runtime/auth/passkeys/signup/finish", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: { challengeId: started.challengeId, registrationResponse },
|
|
159
|
+
});
|
|
160
|
+
if (payload.sessionToken) {
|
|
161
|
+
await this.setSessionToken(payload.sessionToken);
|
|
162
|
+
}
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
/** Log in with any passkey registered for this app (Privy loginWithPasskey). */
|
|
166
|
+
async loginWithPasskey() {
|
|
167
|
+
await this.initialize();
|
|
168
|
+
const adapter = await this.requirePasskeyAdapter();
|
|
169
|
+
const started = await this.request("/v1/runtime/auth/passkeys/login/start", { method: "POST", body: {} });
|
|
170
|
+
const authenticationResponse = await adapter.authenticate(started.options, {
|
|
171
|
+
challengeId: started.challengeId,
|
|
172
|
+
purpose: "login",
|
|
173
|
+
authMethodId: "",
|
|
174
|
+
});
|
|
175
|
+
const payload = await this.request("/v1/runtime/auth/passkeys/login/verify", {
|
|
176
|
+
method: "POST",
|
|
177
|
+
body: { challengeId: started.challengeId, authenticationResponse },
|
|
178
|
+
});
|
|
179
|
+
if (payload.sessionToken) {
|
|
180
|
+
await this.setSessionToken(payload.sessionToken);
|
|
181
|
+
}
|
|
182
|
+
return payload;
|
|
183
|
+
}
|
|
184
|
+
/** Send a login code by SMS (Privy loginWithSMS, step 1). Phone in E.164. */
|
|
185
|
+
async requestSmsCode(phoneNumber) {
|
|
186
|
+
await this.initialize();
|
|
187
|
+
return this.request("/v1/runtime/auth/sms/request", { method: "POST", body: { phoneNumber } });
|
|
188
|
+
}
|
|
189
|
+
async verifySmsCode(challengeId, code) {
|
|
190
|
+
await this.initialize();
|
|
191
|
+
const payload = await this.request("/v1/runtime/auth/sms/verify", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
body: { challengeId, code },
|
|
194
|
+
});
|
|
195
|
+
if (payload.sessionToken) {
|
|
196
|
+
await this.setSessionToken(payload.sessionToken);
|
|
197
|
+
}
|
|
198
|
+
return payload;
|
|
199
|
+
}
|
|
200
|
+
async enrollPasskey(label = "Passkey") {
|
|
201
|
+
await this.initialize();
|
|
202
|
+
const adapter = await this.requirePasskeyAdapter();
|
|
203
|
+
const started = await this.startPasskeyRegistration(label);
|
|
204
|
+
const registrationResponse = await adapter.register(started.options, {
|
|
205
|
+
authMethodId: started.authMethod.id,
|
|
206
|
+
challengeId: started.challengeId,
|
|
207
|
+
label,
|
|
208
|
+
});
|
|
209
|
+
const completed = await this.finishPasskeyRegistration(started.authMethod.id, started.challengeId, registrationResponse);
|
|
210
|
+
return {
|
|
211
|
+
...completed,
|
|
212
|
+
started,
|
|
213
|
+
registrationResponse,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async ensurePasskey(label = "Passkey") {
|
|
217
|
+
await this.initialize();
|
|
218
|
+
const me = await this.getMe();
|
|
219
|
+
const existing = (me.authMethods ?? []).find((method) => method.type === "passkey" && method.active);
|
|
220
|
+
if (existing) {
|
|
221
|
+
return {
|
|
222
|
+
authMethod: existing,
|
|
223
|
+
existing: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const enrollment = await this.enrollPasskey(label);
|
|
227
|
+
const authMethod = (enrollment.authMethod
|
|
228
|
+
?? enrollment.started.authMethod);
|
|
229
|
+
return {
|
|
230
|
+
authMethod,
|
|
231
|
+
existing: false,
|
|
232
|
+
enrollment,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async createChallenge(authMethodId, purpose) {
|
|
236
|
+
await this.initialize();
|
|
237
|
+
return this.request("/v1/runtime/mfa/challenges", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: { authMethodId, purpose },
|
|
240
|
+
runtime: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async verifyChallenge(challengeId, payload) {
|
|
244
|
+
await this.initialize();
|
|
245
|
+
return this.request("/v1/runtime/mfa/challenges/verify", {
|
|
246
|
+
method: "POST",
|
|
247
|
+
body: { challengeId, ...payload },
|
|
248
|
+
runtime: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async approveWithPasskey(authMethodId, purpose) {
|
|
252
|
+
await this.initialize();
|
|
253
|
+
const adapter = await this.requirePasskeyAdapter();
|
|
254
|
+
const challenge = (await this.createChallenge(authMethodId, purpose));
|
|
255
|
+
if (!challenge.options) {
|
|
256
|
+
throw new Error("passkey challenge options missing");
|
|
257
|
+
}
|
|
258
|
+
const authenticationResponse = await adapter.authenticate(challenge.options, {
|
|
259
|
+
challengeId: challenge.challengeId,
|
|
260
|
+
purpose,
|
|
261
|
+
authMethodId,
|
|
262
|
+
});
|
|
263
|
+
const verification = await this.verifyChallenge(challenge.challengeId, {
|
|
264
|
+
authenticationResponse,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
challenge,
|
|
268
|
+
verification,
|
|
269
|
+
authenticationResponse,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async listAccounts() {
|
|
273
|
+
await this.initialize();
|
|
274
|
+
return this.request("/v1/runtime/accounts", { method: "GET", runtime: true });
|
|
275
|
+
}
|
|
276
|
+
async listEmbeddedWallets() {
|
|
277
|
+
const payload = await this.listAccounts();
|
|
278
|
+
return payload.managedAccounts;
|
|
279
|
+
}
|
|
280
|
+
async getManagedAccount(chain, options) {
|
|
281
|
+
const payload = await this.listAccounts();
|
|
282
|
+
return (payload.managedAccounts.find((account) => account.chain === chain &&
|
|
283
|
+
(!options?.purpose || ((account.purpose ?? "agent") === options.purpose))) ?? null);
|
|
284
|
+
}
|
|
285
|
+
async getEmbeddedWallet(chain) {
|
|
286
|
+
return this.getManagedAccount(chain);
|
|
287
|
+
}
|
|
288
|
+
async createManagedAccount(chain, options) {
|
|
289
|
+
await this.initialize();
|
|
290
|
+
const normalizedOptions = typeof options === "string" ? { label: options } : options;
|
|
291
|
+
return this.request("/v1/runtime/accounts/managed", {
|
|
292
|
+
method: "POST",
|
|
293
|
+
body: { chain, ...normalizedOptions },
|
|
294
|
+
runtime: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
async ensureManagedAccount(chain, options) {
|
|
298
|
+
const normalizedOptions = typeof options === "string" ? { label: options } : options;
|
|
299
|
+
const purpose = normalizedOptions?.purpose ??
|
|
300
|
+
(normalizedOptions?.custodyMode === "split_custody" ? "personal" : "agent");
|
|
301
|
+
const existing = await this.getManagedAccount(chain, {
|
|
302
|
+
purpose,
|
|
303
|
+
});
|
|
304
|
+
if (existing) {
|
|
305
|
+
return {
|
|
306
|
+
account: existing,
|
|
307
|
+
existing: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const created = await this.createManagedAccount(chain, normalizedOptions);
|
|
311
|
+
return {
|
|
312
|
+
account: created.account,
|
|
313
|
+
existing: Boolean(created.existing),
|
|
314
|
+
attestation: created.attestation,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async ensureEmbeddedWallet(chain, options) {
|
|
318
|
+
return this.ensureManagedAccount(chain, options);
|
|
319
|
+
}
|
|
320
|
+
async createSigningIntent(body) {
|
|
321
|
+
await this.initialize();
|
|
322
|
+
return this.request("/v1/runtime/intents/signing", {
|
|
323
|
+
method: "POST",
|
|
324
|
+
body,
|
|
325
|
+
runtime: true,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async createTransactionIntent(body) {
|
|
329
|
+
await this.initialize();
|
|
330
|
+
return this.request("/v1/runtime/intents/transactions", {
|
|
331
|
+
method: "POST",
|
|
332
|
+
body,
|
|
333
|
+
runtime: true,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async getIntent(intentId) {
|
|
337
|
+
await this.initialize();
|
|
338
|
+
return this.request(`/v1/runtime/intents/${intentId}`, {
|
|
339
|
+
method: "GET",
|
|
340
|
+
runtime: true,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async waitForIntent(intentId, options = {}) {
|
|
344
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
345
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1_000;
|
|
346
|
+
const startedAt = Date.now();
|
|
347
|
+
for (;;) {
|
|
348
|
+
if (options.signal?.aborted) {
|
|
349
|
+
throw new Error("intent_wait_aborted");
|
|
350
|
+
}
|
|
351
|
+
const payload = (await this.getIntent(intentId));
|
|
352
|
+
const intent = payload.intent;
|
|
353
|
+
if (intent.status === "completed" || intent.status === "failed") {
|
|
354
|
+
return intent;
|
|
355
|
+
}
|
|
356
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
357
|
+
throw new Error("intent_wait_timeout");
|
|
358
|
+
}
|
|
359
|
+
await this.delay(pollIntervalMs, options.signal);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async createSigningIntentAndWait(body, options) {
|
|
363
|
+
const payload = (await this.createSigningIntent(body));
|
|
364
|
+
return this.waitForIntent(payload.intent.id, options);
|
|
365
|
+
}
|
|
366
|
+
async createTransactionIntentAndWait(body, options) {
|
|
367
|
+
const payload = (await this.createTransactionIntent(body));
|
|
368
|
+
return this.waitForIntent(payload.intent.id, options);
|
|
369
|
+
}
|
|
370
|
+
async signMessageAndWait(body, options) {
|
|
371
|
+
return this.createSigningIntentAndWait(body, options);
|
|
372
|
+
}
|
|
373
|
+
async signTypedDataAndWait(body, options) {
|
|
374
|
+
return this.createSigningIntentAndWait({
|
|
375
|
+
accountId: body.accountId,
|
|
376
|
+
chain: "evm",
|
|
377
|
+
challengeId: body.challengeId,
|
|
378
|
+
payload: {
|
|
379
|
+
typedData: body.typedData,
|
|
380
|
+
},
|
|
381
|
+
}, options);
|
|
382
|
+
}
|
|
383
|
+
async signTransactionAndWait(body, options) {
|
|
384
|
+
return this.createSigningIntentAndWait(body, options);
|
|
385
|
+
}
|
|
386
|
+
async createOfflineTransactionSigningEnvelope(body) {
|
|
387
|
+
await this.initialize();
|
|
388
|
+
return createOfflineTransactionSigningEnvelope({
|
|
389
|
+
accountId: body.accountId,
|
|
390
|
+
chain: body.chain,
|
|
391
|
+
expiresAt: body.expiresAt,
|
|
392
|
+
payload: body.payload,
|
|
393
|
+
requester: body.requester ?? {
|
|
394
|
+
appId: this.config.appId ?? this.config.environmentId,
|
|
395
|
+
},
|
|
396
|
+
metadata: body.metadata,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
encodeOfflineSigningEnvelope(envelope) {
|
|
400
|
+
return encodeOfflineSigningEnvelope(envelope);
|
|
401
|
+
}
|
|
402
|
+
decodeOfflineSigningEnvelope(envelope) {
|
|
403
|
+
return decodeOfflineSigningEnvelope(envelope);
|
|
404
|
+
}
|
|
405
|
+
async startBluetoothMeshOfflineSigning(params) {
|
|
406
|
+
const adapter = await this.requireBluetoothMeshAdapter();
|
|
407
|
+
const reassembler = new OfflineSigningMeshReassembler();
|
|
408
|
+
await adapter.start({
|
|
409
|
+
serviceId: params.serviceId ?? "swivy-offline-signing-v1",
|
|
410
|
+
onPeerDiscovered(peer) {
|
|
411
|
+
params.onPeerDiscovered?.(peer);
|
|
412
|
+
},
|
|
413
|
+
onPacket(packet, peer) {
|
|
414
|
+
const envelope = reassembler.accept(packet);
|
|
415
|
+
if (envelope) {
|
|
416
|
+
void params.onEnvelope(envelope, peer);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
stop: () => adapter.stop(),
|
|
422
|
+
sendEnvelope: async (envelope, peerId, options = {}) => {
|
|
423
|
+
const packets = createOfflineSigningMeshPackets({
|
|
424
|
+
envelope,
|
|
425
|
+
ttlSeconds: options.ttlSeconds,
|
|
426
|
+
maxPayloadBytes: options.maxPayloadBytes,
|
|
427
|
+
});
|
|
428
|
+
const retryCount = Math.max(0, options.retryCount ?? 2);
|
|
429
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? 250);
|
|
430
|
+
for (const packet of packets) {
|
|
431
|
+
let lastError;
|
|
432
|
+
for (let attempt = 0; attempt <= retryCount; attempt += 1) {
|
|
433
|
+
try {
|
|
434
|
+
await adapter.send(packet, peerId);
|
|
435
|
+
lastError = null;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
lastError = error;
|
|
440
|
+
if (attempt < retryCount) {
|
|
441
|
+
await this.delay(retryDelayMs);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (lastError) {
|
|
446
|
+
throw lastError;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
messageId: packets[0]?.messageId ?? "",
|
|
451
|
+
packetsSent: packets.length,
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async executeTransactionAndWait(body, options) {
|
|
457
|
+
return this.createTransactionIntentAndWait(body, options);
|
|
458
|
+
}
|
|
459
|
+
async getOidcDiscovery(appId = this.config.appId ?? this.config.environmentId) {
|
|
460
|
+
if (!appId) {
|
|
461
|
+
throw new Error("Swivy appId is required");
|
|
462
|
+
}
|
|
463
|
+
return this.request(`/oidc/${appId}/.well-known/openid-configuration`, {
|
|
464
|
+
method: "GET",
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async getOidcJwks(appId = this.config.appId ?? this.config.environmentId) {
|
|
468
|
+
if (!appId) {
|
|
469
|
+
throw new Error("Swivy appId is required");
|
|
470
|
+
}
|
|
471
|
+
return this.request(`/oidc/${appId}/jwks`, {
|
|
472
|
+
method: "GET",
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
async requirePasskeyAdapter() {
|
|
476
|
+
const adapter = this.config.passkeyAdapter;
|
|
477
|
+
if (!adapter) {
|
|
478
|
+
throw new Error("SwivyExpoClient requires a passkeyAdapter for native passkey enrollment and approval");
|
|
479
|
+
}
|
|
480
|
+
if (adapter.isSupported && !(await adapter.isSupported())) {
|
|
481
|
+
throw new Error("passkeys_not_supported_on_device");
|
|
482
|
+
}
|
|
483
|
+
return adapter;
|
|
484
|
+
}
|
|
485
|
+
async requireBluetoothMeshAdapter() {
|
|
486
|
+
const adapter = this.config.bluetoothMeshAdapter;
|
|
487
|
+
if (!adapter) {
|
|
488
|
+
throw new Error("SwivyExpoClient requires a bluetoothMeshAdapter backed by a native BLE module");
|
|
489
|
+
}
|
|
490
|
+
if (adapter.isSupported && !(await adapter.isSupported())) {
|
|
491
|
+
throw new Error("bluetooth_mesh_not_supported_on_device");
|
|
492
|
+
}
|
|
493
|
+
return adapter;
|
|
494
|
+
}
|
|
495
|
+
async request(path, options) {
|
|
496
|
+
const headers = {
|
|
497
|
+
"x-swivy-app-id": this.config.appId ?? this.config.environmentId ?? "",
|
|
498
|
+
"x-swivy-publishable-key": this.config.publishableKey ?? this.config.apiKey ?? "",
|
|
499
|
+
};
|
|
500
|
+
if (!headers["x-swivy-app-id"] || !headers["x-swivy-publishable-key"]) {
|
|
501
|
+
throw new Error("Swivy appId and publishableKey are required");
|
|
502
|
+
}
|
|
503
|
+
if (options.body !== undefined) {
|
|
504
|
+
headers["content-type"] = "application/json";
|
|
505
|
+
}
|
|
506
|
+
if (options.runtime) {
|
|
507
|
+
if (!this.sessionToken) {
|
|
508
|
+
throw new Error("session token is required");
|
|
509
|
+
}
|
|
510
|
+
headers.authorization = `Bearer ${this.sessionToken}`;
|
|
511
|
+
}
|
|
512
|
+
const response = await fetch(`${this.config.baseUrl}${path}`, {
|
|
513
|
+
method: options.method,
|
|
514
|
+
headers,
|
|
515
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
516
|
+
});
|
|
517
|
+
const text = await response.text();
|
|
518
|
+
const payload = parseJsonResponseBody(response, text);
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
throw new Error(resolveResponseErrorMessage(response, payload));
|
|
521
|
+
}
|
|
522
|
+
return payload;
|
|
523
|
+
}
|
|
524
|
+
async delay(ms, signal) {
|
|
525
|
+
await new Promise((resolve, reject) => {
|
|
526
|
+
const timer = setTimeout(() => {
|
|
527
|
+
cleanup();
|
|
528
|
+
resolve();
|
|
529
|
+
}, ms);
|
|
530
|
+
const onAbort = () => {
|
|
531
|
+
cleanup();
|
|
532
|
+
reject(new Error("intent_wait_aborted"));
|
|
533
|
+
};
|
|
534
|
+
const cleanup = () => {
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
signal?.removeEventListener("abort", onAbort);
|
|
537
|
+
};
|
|
538
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swivy/expo",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Swivy Expo and React Native SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/packages/expo/src/index.js",
|
|
7
|
+
"types": "dist/packages/expo/src/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run test/*.test.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@swivy/core": "^1.0.1"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/packages/expo/src"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/packages/expo/src/index.d.ts",
|
|
21
|
+
"import": "./dist/packages/expo/src/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/Travisswop/Swivy.git",
|
|
35
|
+
"directory": "packages/expo"
|
|
36
|
+
}
|
|
37
|
+
}
|