@openresidency/sdk 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/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @openresidency/sdk
2
+
3
+ Typed client for the OpenResidency API. Dependency-free, uses the global `fetch`
4
+ (Node 18+ or any browser). Every method maps to an endpoint in `docs/openapi.yaml`.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @openresidency/sdk
10
+ ```
11
+
12
+ ## Use
13
+
14
+ ```ts
15
+ import { OpenResidencyClient } from '@openresidency/sdk';
16
+
17
+ // Identity verification is an operator action, so the client needs a credential.
18
+ const client = new OpenResidencyClient({
19
+ baseUrl: 'https://id.katsina.gov.ng',
20
+ operatorKey: process.env.OPERATOR_KEY, // ork_..., minted at POST /operator/keys
21
+ });
22
+
23
+ // Verify a person against the national ID (no residency issued)
24
+ const idv = await client.verifyIdentity({
25
+ countryCode: 'NG',
26
+ identifiers: { nin: '12345678901', dateOfBirth: '1990-01-01' },
27
+ purpose: 'health enrolment',
28
+ });
29
+
30
+ // Issue a residency credential
31
+ const issued = await client.issueResidency({
32
+ countryCode: 'NG',
33
+ subnationalUnit: 'KT',
34
+ identifiers: { nin: '12345678901', dateOfBirth: '1990-01-01' },
35
+ });
36
+ console.log(issued.residentId, issued.credentialJwt);
37
+
38
+ // Verify a presented credential (a sector service checking a citizen's residency)
39
+ const check = await client.verifyCredential(issued.credentialJwt!);
40
+ console.log(check.valid, check.subject);
41
+
42
+ // Consent
43
+ await client.grantConsent({
44
+ residentId: issued.residentId!,
45
+ relyingParty: 'health',
46
+ purpose: 'Enrol in state health scheme',
47
+ scopes: ['residency', 'health'],
48
+ });
49
+ const consents = await client.listConsents(issued.residentId!);
50
+ ```
51
+
52
+ ## Admin endpoints
53
+
54
+ Pass `adminKey` to reach the registry and audit endpoints:
55
+
56
+ ```ts
57
+ const admin = new OpenResidencyClient({
58
+ baseUrl: 'https://id.katsina.gov.ng',
59
+ adminKey: process.env.ADMIN_API_KEY,
60
+ });
61
+ const chain = await admin.verifyAuditChain(); // { ok: true, length: N }
62
+ const page = await admin.listResidents({ countryCode: 'NG', limit: 50 });
63
+ ```
64
+
65
+ ## Errors
66
+
67
+ Non-2xx responses throw `OpenResidencyError` with `status` and parsed `body`.
68
+
69
+ ## Build
70
+
71
+ ```bash
72
+ npm run build
73
+ ```
74
+
75
+ Licensed under Apache-2.0.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * OpenResidency Interoperability SDK.
3
+ *
4
+ * A small, dependency-free typed client for the OpenResidency API. Uses the global
5
+ * fetch (Node 18+ or any browser). Every method maps one-to-one to an endpoint in
6
+ * docs/openapi.yaml, so a sector service (Health, Tax, ...) or a partner system can
7
+ * integrate without hand-writing HTTP calls.
8
+ */
9
+ export type AssuranceLevel = 'none' | 'basic' | 'verified' | 'high';
10
+ export interface ClientOptions {
11
+ baseUrl: string;
12
+ /**
13
+ * Per-operator API key (`ork_...`), minted at POST /operator/keys.
14
+ *
15
+ * This is the credential to use: it identifies WHICH operator is calling, so privileged
16
+ * actions are attributable in the audit log, it carries only the roles that operator
17
+ * holds, and it can be rotated with an overlap window rather than a hard cutover.
18
+ */
19
+ operatorKey?: string;
20
+ /**
21
+ * Legacy shared admin key. Works only where the deployment still runs
22
+ * `operatorAuth.mode: sharedKey`, and carries no identity or roles. Prefer operatorKey.
23
+ *
24
+ * @deprecated Use `operatorKey`.
25
+ */
26
+ adminKey?: string;
27
+ /**
28
+ * Bearer token from an operator SSO sign-in (`operatorAuth.mode: oidc` or `local`).
29
+ * Send this when the caller is a person in a console rather than a machine.
30
+ */
31
+ operatorToken?: string;
32
+ /** Optional custom fetch (for tests or non-standard runtimes). */
33
+ fetch?: typeof fetch;
34
+ }
35
+ export interface IdentityVerifyRequest {
36
+ countryCode: string;
37
+ identifiers: Record<string, string>;
38
+ challengeRef?: string;
39
+ purpose?: string;
40
+ }
41
+ export interface IdentityVerifyResponse {
42
+ verified: boolean;
43
+ assuranceLevel?: AssuranceLevel;
44
+ subjectRef?: string;
45
+ attributes?: Record<string, unknown>;
46
+ pendingChallenge?: boolean;
47
+ challengeRef?: string;
48
+ channel?: string;
49
+ reason?: string;
50
+ }
51
+ export interface IssueRequest {
52
+ countryCode: string;
53
+ subnationalUnit: string;
54
+ identifiers: Record<string, string>;
55
+ holderId?: string;
56
+ challengeRef?: string;
57
+ proofOfResidence?: string;
58
+ /**
59
+ * Applicant phone in E.164, for one-time-code delivery. What is retained depends on the
60
+ * deployment's `contactDirectory.mode`; it never reaches the credential or the audit log.
61
+ */
62
+ phone?: string;
63
+ offline?: boolean;
64
+ }
65
+ export interface IssueResult {
66
+ status: 'issued' | 'exists' | 'challenge' | 'rejected';
67
+ residentId?: string;
68
+ credentialJwt?: string;
69
+ reason?: string;
70
+ challenge?: {
71
+ type: string;
72
+ channel: string;
73
+ challengeRef: string;
74
+ };
75
+ }
76
+ export interface ResidencyStatus {
77
+ residentId: string;
78
+ countryCode: string;
79
+ subnationalUnit: string;
80
+ assuranceLevel: string;
81
+ provisional: boolean;
82
+ createdAt: string;
83
+ }
84
+ export interface CredentialVerifyOutcome {
85
+ valid: boolean;
86
+ reason?: string;
87
+ checkedRevocation?: boolean;
88
+ subject?: Record<string, unknown>;
89
+ }
90
+ export interface ConsentRecord {
91
+ id: string;
92
+ residentId: string;
93
+ relyingParty: string;
94
+ purpose: string;
95
+ scopes: string[];
96
+ status: 'active' | 'revoked' | 'expired';
97
+ grantedAt: string;
98
+ expiresAt?: string;
99
+ revokedAt?: string;
100
+ receiptId: string;
101
+ }
102
+ export interface AuditEvent {
103
+ seq: number;
104
+ id: string;
105
+ timestamp: string;
106
+ action: string;
107
+ actor: string;
108
+ target?: string;
109
+ countryCode?: string;
110
+ outcome: 'success' | 'failure';
111
+ metadata?: Record<string, unknown>;
112
+ prevHash: string;
113
+ hash: string;
114
+ }
115
+ export declare class OpenResidencyError extends Error {
116
+ status: number;
117
+ body: unknown;
118
+ constructor(status: number, body: unknown);
119
+ }
120
+ export declare class OpenResidencyClient {
121
+ private baseUrl;
122
+ private operatorKey?;
123
+ private adminKey?;
124
+ private operatorToken?;
125
+ private doFetch;
126
+ constructor(opts: ClientOptions);
127
+ /** Operator action: needs the `registrar` role. */
128
+ identityChallenge(countryCode: string, identifiers: Record<string, string>): Promise<{
129
+ challengeRequired: boolean;
130
+ challengeRef?: string;
131
+ channel?: string;
132
+ }>;
133
+ /** Operator action: needs the `registrar` role. */
134
+ verifyIdentity(req: IdentityVerifyRequest): Promise<IdentityVerifyResponse>;
135
+ countries(): Promise<{
136
+ countryCode: string;
137
+ countryName: string;
138
+ provider: string;
139
+ inputs: unknown[];
140
+ }[]>;
141
+ /** Operator action: needs the `registrar` role. */
142
+ issueResidency(req: IssueRequest): Promise<IssueResult>;
143
+ residencyStatus(residentId: string): Promise<ResidencyStatus>;
144
+ verifyCredential(credential: string, offline?: boolean): Promise<CredentialVerifyOutcome>;
145
+ /** Operator action: needs the `revoker` role. */
146
+ revokeResidency(residentId: string): Promise<{
147
+ revoked: boolean;
148
+ }>;
149
+ listConsents(residentId: string): Promise<{
150
+ residentId: string;
151
+ consents: ConsentRecord[];
152
+ }>;
153
+ grantConsent(req: {
154
+ residentId: string;
155
+ relyingParty: string;
156
+ purpose: string;
157
+ scopes: string[];
158
+ relyingPartyName?: string;
159
+ validityDays?: number;
160
+ }): Promise<{
161
+ consent: ConsentRecord;
162
+ receipt: string;
163
+ }>;
164
+ revokeConsent(id: string): Promise<{
165
+ consent: ConsentRecord;
166
+ }>;
167
+ listResidents(params?: {
168
+ countryCode?: string;
169
+ limit?: number;
170
+ offset?: number;
171
+ }): Promise<{
172
+ total: number;
173
+ residents: ResidencyStatus[];
174
+ }>;
175
+ auditLog(params?: {
176
+ limit?: number;
177
+ offset?: number;
178
+ target?: string;
179
+ }): Promise<{
180
+ count: number;
181
+ events: AuditEvent[];
182
+ }>;
183
+ verifyAuditChain(): Promise<{
184
+ ok: boolean;
185
+ length: number;
186
+ brokenAtSeq?: number;
187
+ }>;
188
+ oidcDiscovery(): Promise<Record<string, unknown>>;
189
+ private get;
190
+ private post;
191
+ private request;
192
+ }
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * OpenResidency Interoperability SDK.
4
+ *
5
+ * A small, dependency-free typed client for the OpenResidency API. Uses the global
6
+ * fetch (Node 18+ or any browser). Every method maps one-to-one to an endpoint in
7
+ * docs/openapi.yaml, so a sector service (Health, Tax, ...) or a partner system can
8
+ * integrate without hand-writing HTTP calls.
9
+ */
10
+ export class OpenResidencyError extends Error {
11
+ status;
12
+ body;
13
+ constructor(status, body) {
14
+ super(`OpenResidency API error ${status}`);
15
+ this.status = status;
16
+ this.body = body;
17
+ }
18
+ }
19
+ export class OpenResidencyClient {
20
+ baseUrl;
21
+ operatorKey;
22
+ adminKey;
23
+ operatorToken;
24
+ doFetch;
25
+ constructor(opts) {
26
+ this.baseUrl = opts.baseUrl.replace(/\/$/, '');
27
+ this.operatorKey = opts.operatorKey;
28
+ this.adminKey = opts.adminKey;
29
+ this.operatorToken = opts.operatorToken;
30
+ this.doFetch = opts.fetch ?? fetch;
31
+ }
32
+ // ---- identity ----
33
+ /** Operator action: needs the `registrar` role. */
34
+ identityChallenge(countryCode, identifiers) {
35
+ return this.post('/identity/challenge', { countryCode, identifiers }, true);
36
+ }
37
+ /** Operator action: needs the `registrar` role. */
38
+ verifyIdentity(req) {
39
+ return this.post('/identity/verify', req, true);
40
+ }
41
+ // ---- residency ----
42
+ countries() {
43
+ return this.get('/residency/countries');
44
+ }
45
+ /** Operator action: needs the `registrar` role. */
46
+ issueResidency(req) {
47
+ return this.post('/residency/issue', req, true);
48
+ }
49
+ residencyStatus(residentId) {
50
+ return this.get(`/residency/${encodeURIComponent(residentId)}`);
51
+ }
52
+ verifyCredential(credential, offline = false) {
53
+ return this.post('/residency/verify', { credential, offline });
54
+ }
55
+ /** Operator action: needs the `revoker` role. */
56
+ revokeResidency(residentId) {
57
+ return this.post(`/residency/revoke/${encodeURIComponent(residentId)}`, {}, true);
58
+ }
59
+ // ---- consent ----
60
+ // The consent routes are operator-guarded server-side (support role), so they carry
61
+ // credentials like the admin ones. They previously did not, and 401'd.
62
+ listConsents(residentId) {
63
+ return this.get(`/consent/resident/${encodeURIComponent(residentId)}`, true);
64
+ }
65
+ grantConsent(req) {
66
+ return this.post('/consent/grant', req, true);
67
+ }
68
+ revokeConsent(id) {
69
+ return this.post(`/consent/${encodeURIComponent(id)}/revoke`, {}, true);
70
+ }
71
+ // ---- admin (operator-authenticated) ----
72
+ listResidents(params = {}) {
73
+ const q = new URLSearchParams();
74
+ if (params.countryCode)
75
+ q.set('countryCode', params.countryCode);
76
+ if (params.limit != null)
77
+ q.set('limit', String(params.limit));
78
+ if (params.offset != null)
79
+ q.set('offset', String(params.offset));
80
+ return this.get(`/admin/residents?${q.toString()}`, true);
81
+ }
82
+ auditLog(params = {}) {
83
+ const q = new URLSearchParams();
84
+ if (params.limit != null)
85
+ q.set('limit', String(params.limit));
86
+ if (params.offset != null)
87
+ q.set('offset', String(params.offset));
88
+ if (params.target)
89
+ q.set('target', params.target);
90
+ return this.get(`/audit?${q.toString()}`, true);
91
+ }
92
+ verifyAuditChain() {
93
+ return this.get('/audit/verify', true);
94
+ }
95
+ // ---- discovery ----
96
+ oidcDiscovery() {
97
+ return this.get('/oidc/.well-known/openid-configuration');
98
+ }
99
+ // ---- internals ----
100
+ async get(path, admin = false) {
101
+ return this.request('GET', path, undefined, admin);
102
+ }
103
+ async post(path, body, admin = false) {
104
+ return this.request('POST', path, body, admin);
105
+ }
106
+ async request(method, path, body, admin = false) {
107
+ const headers = { accept: 'application/json' };
108
+ if (body !== undefined)
109
+ headers['content-type'] = 'application/json';
110
+ if (admin) {
111
+ // Preference order matches how much the deployment can tell about the caller:
112
+ // an operator key or SSO token names a person; the shared key names nobody.
113
+ if (this.operatorKey) {
114
+ headers['x-operator-key'] = this.operatorKey;
115
+ }
116
+ else if (this.operatorToken) {
117
+ headers['authorization'] = `Bearer ${this.operatorToken}`;
118
+ }
119
+ else if (this.adminKey) {
120
+ headers['x-admin-key'] = this.adminKey;
121
+ }
122
+ else {
123
+ throw new Error('This endpoint requires operator authentication: set operatorKey (preferred), ' +
124
+ 'operatorToken, or the legacy adminKey in ClientOptions');
125
+ }
126
+ }
127
+ const res = await this.doFetch(`${this.baseUrl}${path}`, {
128
+ method,
129
+ headers,
130
+ body: body !== undefined ? JSON.stringify(body) : undefined,
131
+ });
132
+ const text = await res.text();
133
+ const parsed = text ? JSON.parse(text) : undefined;
134
+ if (!res.ok)
135
+ throw new OpenResidencyError(res.status, parsed);
136
+ return parsed;
137
+ }
138
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@openresidency/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Typed client for the OpenResidency API (identity verification, residency, consent, SSO helpers).",
5
+ "license": "Apache-2.0",
6
+ "author": "Chinonso Williams <nonso@harmonizedx.com>",
7
+ "contributors": [
8
+ "HarmonizedX Limited"
9
+ ],
10
+ "homepage": "https://github.com/Harmonizedx/open-residency",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/Harmonizedx/open-residency.git",
14
+ "directory": "sdk"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/Harmonizedx/open-residency/issues"
18
+ },
19
+ "keywords": [
20
+ "openresidency",
21
+ "digital-identity",
22
+ "verifiable-credentials",
23
+ "sdk"
24
+ ],
25
+ "type": "module",
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "files": ["dist", "README.md"],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }