@estiva-app/identity 0.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/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/client.d.ts +124 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +398 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/shell.d.ts +195 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +166 -0
- package/dist/shell.js.map +1 -0
- package/package.json +53 -0
- package/src/client.ts +512 -0
- package/src/index.ts +81 -0
- package/src/shell.ts +280 -0
package/dist/shell.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auth shell's contract: what a boot may conclude, and what to say about it.
|
|
3
|
+
*
|
|
4
|
+
* Pure decision logic and vocabulary. No I/O, no globals, no rendering — the
|
|
5
|
+
* shell's *appearance* stays in each app, because it is a full-page surface and
|
|
6
|
+
* each app will want its own. What is shared is the state machine and the
|
|
7
|
+
* reason vocabulary, which is where the expensive mistakes live.
|
|
8
|
+
*
|
|
9
|
+
* ## This file was duplicated verbatim, and said so
|
|
10
|
+
*
|
|
11
|
+
* Until SHA-4 it existed twice — `peek-app/src/auth/shell/contract.ts` and
|
|
12
|
+
* `estiva-ship/lib/auth/shell/contract.ts` — and each copy's header pointed at
|
|
13
|
+
* the other. They were byte-identical apart from that sentence, which is a file
|
|
14
|
+
* asking to be a package. Now it is one, and the claim is true by construction
|
|
15
|
+
* rather than by discipline.
|
|
16
|
+
*
|
|
17
|
+
* ## Why `token_rejected` is not "session expired"
|
|
18
|
+
*
|
|
19
|
+
* The distinction below is load-bearing and asserted by a test that travelled
|
|
20
|
+
* here with the code. Telling somebody their session expired when their token
|
|
21
|
+
* was refused for a *configuration* fault sends them to re-authenticate against
|
|
22
|
+
* a wall: they will succeed at signing in and fail in exactly the same way, and
|
|
23
|
+
* nothing in the message points at the real cause.
|
|
24
|
+
*
|
|
25
|
+
* ## `Storage` is not named here
|
|
26
|
+
*
|
|
27
|
+
* The guard helpers take a local {@link KeyValueStore} rather than the DOM's
|
|
28
|
+
* `Storage`. Both `sessionStorage` and `localStorage` satisfy it structurally,
|
|
29
|
+
* and it keeps the published declarations free of an ambient DOM type — a `.d.ts`
|
|
30
|
+
* that names one compiles in a browser app and fails in a Node consumer
|
|
31
|
+
* (ADR 0002 §4a).
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The subset of a web Storage this package uses.
|
|
35
|
+
*
|
|
36
|
+
* Structural, so `sessionStorage`, `localStorage` and a plain object all satisfy
|
|
37
|
+
* it. Which one an app passes is an app decision with real consequences — see
|
|
38
|
+
* the note on storage in `client.ts`.
|
|
39
|
+
*/
|
|
40
|
+
export interface KeyValueStore {
|
|
41
|
+
getItem(key: string): string | null;
|
|
42
|
+
setItem(key: string, value: string): void;
|
|
43
|
+
removeItem(key: string): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Five states, and no more.
|
|
47
|
+
*
|
|
48
|
+
* `checking` and `entering` deliberately render the same thing: the difference
|
|
49
|
+
* is not information a person entering their own app needs, and distinguishing
|
|
50
|
+
* them visually produces two loading screens for one boot.
|
|
51
|
+
*/
|
|
52
|
+
export type ShellPhase = 'checking' | 'entering' | 'authenticating' | 'ready' | 'failed';
|
|
53
|
+
/**
|
|
54
|
+
* Why the shell is not simply entering.
|
|
55
|
+
*
|
|
56
|
+
* These are not interchangeable. The whole reason the list is this long is that
|
|
57
|
+
* "your session expired" told to somebody whose token was refused for a
|
|
58
|
+
* configuration fault sends them to re-authenticate against a wall.
|
|
59
|
+
*/
|
|
60
|
+
export type ShellReason = 'no_session' | 'session_expired' | 'identity_inactive' | 'token_rejected' | 'exchange_failed' | 'network' | 'timeout' | 'loop_guard';
|
|
61
|
+
export type ShellState = {
|
|
62
|
+
phase: 'checking';
|
|
63
|
+
} | {
|
|
64
|
+
phase: 'entering';
|
|
65
|
+
} | {
|
|
66
|
+
phase: 'authenticating';
|
|
67
|
+
reason: ShellReason;
|
|
68
|
+
} | {
|
|
69
|
+
phase: 'ready';
|
|
70
|
+
} | {
|
|
71
|
+
phase: 'failed';
|
|
72
|
+
reason: ShellReason;
|
|
73
|
+
};
|
|
74
|
+
/** Whether a reason can be cleared by authenticating, or needs somebody else. */
|
|
75
|
+
export declare function isRecoverable(reason: ShellReason): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* What the shell says, per reason.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately plain and deliberately short. This is a bootstrap surface, not a
|
|
80
|
+
* login page, and a paragraph of reassurance here reads as a product asking to
|
|
81
|
+
* be trusted rather than an app starting up.
|
|
82
|
+
*
|
|
83
|
+
* `token_rejected` is the one that must never read as "signed out" — it is a
|
|
84
|
+
* configuration fault that signing in again cannot fix, and saying otherwise
|
|
85
|
+
* cost a real debugging detour the first time it appeared.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SHELL_COPY: Record<ShellReason, string>;
|
|
88
|
+
/** The label on the one affordance the shell ever shows. */
|
|
89
|
+
export declare const CONTINUE_LABEL = "Continue with passkey";
|
|
90
|
+
export declare const RETRY_LABEL = "Try again";
|
|
91
|
+
/** What the app observed before deciding anything. */
|
|
92
|
+
export interface BootObservation {
|
|
93
|
+
/** Parsed from the address bar, if this load is a return trip. */
|
|
94
|
+
callback: {
|
|
95
|
+
code?: string | undefined;
|
|
96
|
+
error?: string | undefined;
|
|
97
|
+
} | null;
|
|
98
|
+
/** A stored token that is present *and* still usable. */
|
|
99
|
+
hasValidToken: boolean;
|
|
100
|
+
/** The silent-attempt marker, if one is set. */
|
|
101
|
+
guardAttemptedAt: number | null;
|
|
102
|
+
/**
|
|
103
|
+
* Whether this page load has already been inside the app.
|
|
104
|
+
*
|
|
105
|
+
* A cold boot and a token dying under somebody's hands are the same
|
|
106
|
+
* observation — no valid token — and they must not get the same treatment.
|
|
107
|
+
* Silent entry is right on boot, where there is nothing on screen to lose.
|
|
108
|
+
* Mid-session it navigates away and takes whatever they were doing with it,
|
|
109
|
+
* which is how "enter automatically" turned into "the page left on its own".
|
|
110
|
+
*/
|
|
111
|
+
enteredThisPageLoad: boolean;
|
|
112
|
+
now: number;
|
|
113
|
+
}
|
|
114
|
+
export type BootAction =
|
|
115
|
+
/** Redeem the code in the address bar. */
|
|
116
|
+
{
|
|
117
|
+
do: 'complete_callback';
|
|
118
|
+
}
|
|
119
|
+
/** Enter with what is already held. */
|
|
120
|
+
| {
|
|
121
|
+
do: 'enter';
|
|
122
|
+
}
|
|
123
|
+
/** Navigate to `/authorize?prompt=none`. Sets the marker first. */
|
|
124
|
+
| {
|
|
125
|
+
do: 'probe_silently';
|
|
126
|
+
}
|
|
127
|
+
/** Show the one affordance and wait for the tap. */
|
|
128
|
+
| {
|
|
129
|
+
do: 'prompt_passkey';
|
|
130
|
+
reason: ShellReason;
|
|
131
|
+
}
|
|
132
|
+
/** Stop, with something honest on screen. */
|
|
133
|
+
| {
|
|
134
|
+
do: 'fail';
|
|
135
|
+
reason: ShellReason;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* How long a silent attempt may take before the return trip counts as a timeout.
|
|
139
|
+
*
|
|
140
|
+
* A navigation cannot be timed out from the page being unloaded, so this is
|
|
141
|
+
* measured across the round trip instead: the marker carries when we left, and
|
|
142
|
+
* a return that took this long means a slow or half-failing identity service.
|
|
143
|
+
* Generous, because it is competing with a cold DNS lookup and a redirect, and
|
|
144
|
+
* a false timeout is worse than a slow entry.
|
|
145
|
+
*/
|
|
146
|
+
export declare const PROBE_TIMEOUT_MS = 15000;
|
|
147
|
+
/**
|
|
148
|
+
* How long the guard remembers a silent attempt.
|
|
149
|
+
*
|
|
150
|
+
* Long enough that an immediate bounce is caught, short enough that a person
|
|
151
|
+
* who left the tab open and came back gets a real attempt rather than an
|
|
152
|
+
* affordance explaining a failure from an hour ago.
|
|
153
|
+
*/
|
|
154
|
+
export declare const GUARD_TTL_MS = 60000;
|
|
155
|
+
/**
|
|
156
|
+
* The whole entry decision, as a pure function.
|
|
157
|
+
*
|
|
158
|
+
* Ordering is the contract. A callback is handled before a stored token because
|
|
159
|
+
* the code in the address bar is fresher and must be spent or discarded either
|
|
160
|
+
* way; the guard is consulted before a new probe because that is what stops the
|
|
161
|
+
* bounce.
|
|
162
|
+
*/
|
|
163
|
+
export declare function decideBoot(o: BootObservation): BootAction;
|
|
164
|
+
/**
|
|
165
|
+
* Map what `/authorize` sent back onto a reason.
|
|
166
|
+
*
|
|
167
|
+
* `login_required` is the expected negative answer from `prompt=none`, not a
|
|
168
|
+
* fault. `access_denied` is a suspended or offboarded identity, which is the
|
|
169
|
+
* one refusal a person genuinely cannot clear themselves.
|
|
170
|
+
*/
|
|
171
|
+
export declare function reasonForCallbackError(error: string): ShellReason;
|
|
172
|
+
export declare const GUARD_KEY = "estiva.authShell.silentAttempt";
|
|
173
|
+
/**
|
|
174
|
+
* Per-tab, on purpose. `sessionStorage` dies with the tab, so a genuinely new
|
|
175
|
+
* session gets a genuinely fresh silent attempt rather than inheriting a
|
|
176
|
+
* failure somebody hit yesterday.
|
|
177
|
+
*/
|
|
178
|
+
export declare function readGuard(store: Pick<KeyValueStore, 'getItem'>): number | null;
|
|
179
|
+
export declare function markGuard(store: Pick<KeyValueStore, 'setItem'>, now: number): void;
|
|
180
|
+
export declare function clearGuard(store: Pick<KeyValueStore, 'removeItem'>): void;
|
|
181
|
+
export interface ShellEvent {
|
|
182
|
+
app: string;
|
|
183
|
+
phase: ShellPhase;
|
|
184
|
+
reason?: ShellReason;
|
|
185
|
+
/** Milliseconds since the shell started, for the drop-off view. */
|
|
186
|
+
elapsedMs: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Failures are logged at `error`, not `warn`.
|
|
190
|
+
*
|
|
191
|
+
* The point of PEEK-120 is that a broken entry is loud. A `warn` in a browser
|
|
192
|
+
* console is indistinguishable from the noise every app emits at boot.
|
|
193
|
+
*/
|
|
194
|
+
export declare function logShellEvent(e: ShellEvent): void;
|
|
195
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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"}
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auth shell's contract: what a boot may conclude, and what to say about it.
|
|
3
|
+
*
|
|
4
|
+
* Pure decision logic and vocabulary. No I/O, no globals, no rendering — the
|
|
5
|
+
* shell's *appearance* stays in each app, because it is a full-page surface and
|
|
6
|
+
* each app will want its own. What is shared is the state machine and the
|
|
7
|
+
* reason vocabulary, which is where the expensive mistakes live.
|
|
8
|
+
*
|
|
9
|
+
* ## This file was duplicated verbatim, and said so
|
|
10
|
+
*
|
|
11
|
+
* Until SHA-4 it existed twice — `peek-app/src/auth/shell/contract.ts` and
|
|
12
|
+
* `estiva-ship/lib/auth/shell/contract.ts` — and each copy's header pointed at
|
|
13
|
+
* the other. They were byte-identical apart from that sentence, which is a file
|
|
14
|
+
* asking to be a package. Now it is one, and the claim is true by construction
|
|
15
|
+
* rather than by discipline.
|
|
16
|
+
*
|
|
17
|
+
* ## Why `token_rejected` is not "session expired"
|
|
18
|
+
*
|
|
19
|
+
* The distinction below is load-bearing and asserted by a test that travelled
|
|
20
|
+
* here with the code. Telling somebody their session expired when their token
|
|
21
|
+
* was refused for a *configuration* fault sends them to re-authenticate against
|
|
22
|
+
* a wall: they will succeed at signing in and fail in exactly the same way, and
|
|
23
|
+
* nothing in the message points at the real cause.
|
|
24
|
+
*
|
|
25
|
+
* ## `Storage` is not named here
|
|
26
|
+
*
|
|
27
|
+
* The guard helpers take a local {@link KeyValueStore} rather than the DOM's
|
|
28
|
+
* `Storage`. Both `sessionStorage` and `localStorage` satisfy it structurally,
|
|
29
|
+
* and it keeps the published declarations free of an ambient DOM type — a `.d.ts`
|
|
30
|
+
* that names one compiles in a browser app and fails in a Node consumer
|
|
31
|
+
* (ADR 0002 §4a).
|
|
32
|
+
*/
|
|
33
|
+
/** Whether a reason can be cleared by authenticating, or needs somebody else. */
|
|
34
|
+
export function isRecoverable(reason) {
|
|
35
|
+
return reason !== 'identity_inactive' && reason !== 'token_rejected';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What the shell says, per reason.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately plain and deliberately short. This is a bootstrap surface, not a
|
|
41
|
+
* login page, and a paragraph of reassurance here reads as a product asking to
|
|
42
|
+
* be trusted rather than an app starting up.
|
|
43
|
+
*
|
|
44
|
+
* `token_rejected` is the one that must never read as "signed out" — it is a
|
|
45
|
+
* configuration fault that signing in again cannot fix, and saying otherwise
|
|
46
|
+
* cost a real debugging detour the first time it appeared.
|
|
47
|
+
*/
|
|
48
|
+
export const SHELL_COPY = {
|
|
49
|
+
no_session: 'Continue with your passkey to enter.',
|
|
50
|
+
session_expired: 'Your Estiva ID session ended. Continue with your passkey.',
|
|
51
|
+
identity_inactive: 'This identity is not active. An administrator needs to restore it.',
|
|
52
|
+
token_rejected: 'Signed in to Estiva ID, but this app could not verify the token. That is a configuration problem rather than an expired session — signing in again will not fix it.',
|
|
53
|
+
exchange_failed: 'That sign-in did not complete. Try again.',
|
|
54
|
+
network: 'Could not reach Estiva ID. Check your connection and try again.',
|
|
55
|
+
timeout: 'Estiva ID did not answer in time. Try again.',
|
|
56
|
+
loop_guard: 'Continue with your passkey to enter.',
|
|
57
|
+
};
|
|
58
|
+
/** The label on the one affordance the shell ever shows. */
|
|
59
|
+
export const CONTINUE_LABEL = 'Continue with passkey';
|
|
60
|
+
export const RETRY_LABEL = 'Try again';
|
|
61
|
+
/**
|
|
62
|
+
* How long a silent attempt may take before the return trip counts as a timeout.
|
|
63
|
+
*
|
|
64
|
+
* A navigation cannot be timed out from the page being unloaded, so this is
|
|
65
|
+
* measured across the round trip instead: the marker carries when we left, and
|
|
66
|
+
* a return that took this long means a slow or half-failing identity service.
|
|
67
|
+
* Generous, because it is competing with a cold DNS lookup and a redirect, and
|
|
68
|
+
* a false timeout is worse than a slow entry.
|
|
69
|
+
*/
|
|
70
|
+
export const PROBE_TIMEOUT_MS = 15_000;
|
|
71
|
+
/**
|
|
72
|
+
* How long the guard remembers a silent attempt.
|
|
73
|
+
*
|
|
74
|
+
* Long enough that an immediate bounce is caught, short enough that a person
|
|
75
|
+
* who left the tab open and came back gets a real attempt rather than an
|
|
76
|
+
* affordance explaining a failure from an hour ago.
|
|
77
|
+
*/
|
|
78
|
+
export const GUARD_TTL_MS = 60_000;
|
|
79
|
+
/**
|
|
80
|
+
* The whole entry decision, as a pure function.
|
|
81
|
+
*
|
|
82
|
+
* Ordering is the contract. A callback is handled before a stored token because
|
|
83
|
+
* the code in the address bar is fresher and must be spent or discarded either
|
|
84
|
+
* way; the guard is consulted before a new probe because that is what stops the
|
|
85
|
+
* bounce.
|
|
86
|
+
*/
|
|
87
|
+
export function decideBoot(o) {
|
|
88
|
+
if (o.callback) {
|
|
89
|
+
const { code, error } = o.callback;
|
|
90
|
+
if (error)
|
|
91
|
+
return { do: 'prompt_passkey', reason: reasonForCallbackError(error) };
|
|
92
|
+
if (code)
|
|
93
|
+
return { do: 'complete_callback' };
|
|
94
|
+
}
|
|
95
|
+
if (o.hasValidToken)
|
|
96
|
+
return { do: 'enter' };
|
|
97
|
+
/**
|
|
98
|
+
* The token died while they were working. Offer the passkey and let them
|
|
99
|
+
* choose when to lose the page, rather than deciding it for them.
|
|
100
|
+
*
|
|
101
|
+
* The old signed-out screen was a button partly for this reason. Re-entry is
|
|
102
|
+
* usually silent, so the cost here is not a prompt — it is a full navigation
|
|
103
|
+
* out of whatever they had open, and that is not the shell's call to make.
|
|
104
|
+
* PEEK-108 is what removes the interruption rather than relocating it.
|
|
105
|
+
*/
|
|
106
|
+
if (o.enteredThisPageLoad)
|
|
107
|
+
return { do: 'prompt_passkey', reason: 'session_expired' };
|
|
108
|
+
// A silent attempt already ran and we are back here without a token. Never
|
|
109
|
+
// navigate again — this is the infinite bounce the guard exists to prevent.
|
|
110
|
+
if (o.guardAttemptedAt !== null && o.now - o.guardAttemptedAt < GUARD_TTL_MS) {
|
|
111
|
+
const elapsed = o.now - o.guardAttemptedAt;
|
|
112
|
+
return { do: 'prompt_passkey', reason: elapsed >= PROBE_TIMEOUT_MS ? 'timeout' : 'loop_guard' };
|
|
113
|
+
}
|
|
114
|
+
return { do: 'probe_silently' };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Map what `/authorize` sent back onto a reason.
|
|
118
|
+
*
|
|
119
|
+
* `login_required` is the expected negative answer from `prompt=none`, not a
|
|
120
|
+
* fault. `access_denied` is a suspended or offboarded identity, which is the
|
|
121
|
+
* one refusal a person genuinely cannot clear themselves.
|
|
122
|
+
*/
|
|
123
|
+
export function reasonForCallbackError(error) {
|
|
124
|
+
switch (error) {
|
|
125
|
+
case 'login_required':
|
|
126
|
+
return 'no_session';
|
|
127
|
+
case 'access_denied':
|
|
128
|
+
return 'identity_inactive';
|
|
129
|
+
default:
|
|
130
|
+
return 'exchange_failed';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// --- The loop guard ----------------------------------------------------------
|
|
134
|
+
export const GUARD_KEY = 'estiva.authShell.silentAttempt';
|
|
135
|
+
/**
|
|
136
|
+
* Per-tab, on purpose. `sessionStorage` dies with the tab, so a genuinely new
|
|
137
|
+
* session gets a genuinely fresh silent attempt rather than inheriting a
|
|
138
|
+
* failure somebody hit yesterday.
|
|
139
|
+
*/
|
|
140
|
+
export function readGuard(store) {
|
|
141
|
+
const raw = store.getItem(GUARD_KEY);
|
|
142
|
+
if (!raw)
|
|
143
|
+
return null;
|
|
144
|
+
const at = Number(raw);
|
|
145
|
+
return Number.isFinite(at) ? at : null;
|
|
146
|
+
}
|
|
147
|
+
export function markGuard(store, now) {
|
|
148
|
+
store.setItem(GUARD_KEY, String(now));
|
|
149
|
+
}
|
|
150
|
+
export function clearGuard(store) {
|
|
151
|
+
store.removeItem(GUARD_KEY);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Failures are logged at `error`, not `warn`.
|
|
155
|
+
*
|
|
156
|
+
* The point of PEEK-120 is that a broken entry is loud. A `warn` in a browser
|
|
157
|
+
* console is indistinguishable from the noise every app emits at boot.
|
|
158
|
+
*/
|
|
159
|
+
export function logShellEvent(e) {
|
|
160
|
+
const line = `[auth-shell] ${e.app} ${e.phase}${e.reason ? ` (${e.reason})` : ''} +${e.elapsedMs}ms`;
|
|
161
|
+
if (e.phase === 'failed')
|
|
162
|
+
console.error(line);
|
|
163
|
+
else
|
|
164
|
+
console.info(line);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@estiva-app/identity",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Estiva ID sign-in: authorization code + PKCE, token storage, silent renewal, sign-out, and the auth-shell state machine. Knows nothing about any app's data layer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/estiva-app/estiva-foundation.git",
|
|
33
|
+
"directory": "packages/identity"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"estiva",
|
|
37
|
+
"estiva-id",
|
|
38
|
+
"oauth",
|
|
39
|
+
"pkce",
|
|
40
|
+
"nostr"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"test": "npm run build && node --test test/*.test.mjs test/*.test.ts",
|
|
45
|
+
"typecheck": "npm run build && tsc -p tsconfig.test.json",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^24.13.3",
|
|
51
|
+
"typescript": "~5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|