@oxy.so/core 7.23.2 → 7.24.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/dist/cjs/api/accounts.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AccountsApi = exports.AccountInvitationsApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
|
|
36
|
+
exports.AccountsApi = exports.AccountInvitationsApi = exports.AccountIdentityKeyApi = exports.AccountAgentKeysApi = exports.AccountMembersApi = exports.kindAcceptsAccountCategories = exports.isSelectableAccountCategoryId = exports.isOperatorSwitchTargetKind = exports.isDelegatedActAsEligibleKind = exports.SELECTABLE_ACCOUNT_CATEGORY_IDS = exports.MAX_ACCOUNT_CATEGORIES = exports.ACCOUNT_KINDS = exports.ACCOUNT_CATEGORY_IDS = void 0;
|
|
4
37
|
const client_1 = require("@oxy.so/contracts/client");
|
|
5
38
|
const userIdentity_1 = require("../utils/userIdentity");
|
|
6
39
|
const identityCacheSweep_1 = require("../utils/identityCacheSweep");
|
|
@@ -88,6 +121,73 @@ class AccountAgentKeysApi {
|
|
|
88
121
|
}
|
|
89
122
|
}
|
|
90
123
|
exports.AccountAgentKeysApi = AccountAgentKeysApi;
|
|
124
|
+
/**
|
|
125
|
+
* `oxy.accounts.identityKey` — an organization's identity key (ADR 0061): its
|
|
126
|
+
* DID `#key-1`, the key Peable derives its FairCoin receive addresses from.
|
|
127
|
+
* Only the organization's owners and admins may read or change it; the key is
|
|
128
|
+
* created and held in Commons and never signs anybody in.
|
|
129
|
+
*
|
|
130
|
+
* `get` answers `publicKey: null` for an organization that has no key yet (its
|
|
131
|
+
* DID lists no `#key-1`): that is when a client shows its owners "Set up the
|
|
132
|
+
* organization's Peable wallet". A 403 (`ORGANIZATION_KEY_FORBIDDEN`) means the
|
|
133
|
+
* signed-in person is not an owner or admin of a claimed, active organization.
|
|
134
|
+
*/
|
|
135
|
+
class AccountIdentityKeyApi {
|
|
136
|
+
constructor(ctx) {
|
|
137
|
+
this.ctx = ctx;
|
|
138
|
+
}
|
|
139
|
+
/** The organization's key (`null` without one) and every key it has had, oldest first. */
|
|
140
|
+
async get(accountId) {
|
|
141
|
+
return (0, client_1.parseContract)(client_1.organizationIdentityKeyStateCheck, await this.ctx.request('GET', `/accounts/${enc(accountId)}/identity-key`, undefined, { cache: false }));
|
|
142
|
+
}
|
|
143
|
+
/** A one-use challenge for one change, with the claims every signer signs. */
|
|
144
|
+
async requestChallenge(accountId, request) {
|
|
145
|
+
return (0, client_1.parseContract)(client_1.organizationKeyChallengeCheck, await this.ctx.request('POST', `/accounts/${enc(accountId)}/identity-key/challenge`, (0, client_1.parseContract)(client_1.organizationKeyChallengeRequestCheck, request), { cache: false }));
|
|
146
|
+
}
|
|
147
|
+
/** Apply a change with its signatures (see {@link link} for the whole ceremony). */
|
|
148
|
+
async apply(accountId, request) {
|
|
149
|
+
return (0, client_1.parseContract)(client_1.organizationIdentityKeyStateCheck, await this.ctx.request('POST', `/accounts/${enc(accountId)}/identity-key`, (0, client_1.parseContract)(client_1.organizationKeyApplyRequestCheck, request), { cache: false }));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Commons' whole ceremony. NATIVE-ONLY: the actor is THIS device's personal
|
|
153
|
+
* root (`KeyManager`), which must be the signed-in person's. `key` is the
|
|
154
|
+
* organization key the device generated (`create`, or `replace` with a new
|
|
155
|
+
* one); `previousKey` is the current organization key when the device holds
|
|
156
|
+
* it, so the replacement is recorded as proven by it.
|
|
157
|
+
*/
|
|
158
|
+
async link(accountId, input) {
|
|
159
|
+
const { KeyManager, signIdentityProof } = await Promise.resolve().then(() => __importStar(require('../crypto/index')));
|
|
160
|
+
const [rootPrivateKey, rootPublicKey] = await Promise.all([
|
|
161
|
+
KeyManager.getPrivateKey(),
|
|
162
|
+
KeyManager.getPublicKey(),
|
|
163
|
+
]);
|
|
164
|
+
if (!rootPrivateKey || !rootPublicKey) {
|
|
165
|
+
throw new Error('No identity on this device to sign with');
|
|
166
|
+
}
|
|
167
|
+
const challenge = await this.requestChallenge(accountId, {
|
|
168
|
+
operation: input.operation,
|
|
169
|
+
publicKey: input.key.publicKey,
|
|
170
|
+
});
|
|
171
|
+
const root = rootPublicKey.trim().toLowerCase();
|
|
172
|
+
if (challenge.actorPublicKey !== root) {
|
|
173
|
+
throw new Error("This device's identity is not the signed-in account's");
|
|
174
|
+
}
|
|
175
|
+
const sign = async (signer, publicKey) => (await signIdentityProof({ privateKey: signer.privateKey, publicKey }, (0, client_1.organizationKeyProofClaims)(challenge, publicKey))).signature;
|
|
176
|
+
const previousPublicKey = challenge.previousPublicKey;
|
|
177
|
+
return this.apply(accountId, {
|
|
178
|
+
operation: input.operation,
|
|
179
|
+
publicKey: challenge.publicKey,
|
|
180
|
+
challenge: challenge.challenge,
|
|
181
|
+
expiresAt: challenge.expiresAt,
|
|
182
|
+
actorSignature: await sign({ privateKey: rootPrivateKey, publicKey: root }, root),
|
|
183
|
+
keySignature: await sign(input.key, challenge.publicKey),
|
|
184
|
+
...(input.previousKey && previousPublicKey
|
|
185
|
+
? { previousKeySignature: await sign(input.previousKey, previousPublicKey) }
|
|
186
|
+
: {}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.AccountIdentityKeyApi = AccountIdentityKeyApi;
|
|
91
191
|
/** Consent-based invitations; never cached or queued as offline mutations. */
|
|
92
192
|
class AccountInvitationsApi {
|
|
93
193
|
constructor(ctx) {
|
|
@@ -123,6 +223,7 @@ class AccountsApi {
|
|
|
123
223
|
this.members = new AccountMembersApi(ctx);
|
|
124
224
|
this.invitations = new AccountInvitationsApi(ctx);
|
|
125
225
|
this.agentKeys = new AccountAgentKeysApi(ctx);
|
|
226
|
+
this.identityKey = new AccountIdentityKeyApi(ctx);
|
|
126
227
|
}
|
|
127
228
|
/**
|
|
128
229
|
* List the accounts the caller can access: their own personal (root)
|
package/dist/esm/api/accounts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentKeyListResponseCheck, agentKeyOperationChallengeCheck, agentKeyOperationCheck, agentKeyOperationResultCheck, executeAgentKeyOperationCheck, parseContract, } from '@oxy.so/contracts/client';
|
|
1
|
+
import { agentKeyListResponseCheck, agentKeyOperationChallengeCheck, agentKeyOperationCheck, agentKeyOperationResultCheck, executeAgentKeyOperationCheck, organizationIdentityKeyStateCheck, organizationKeyApplyRequestCheck, organizationKeyChallengeCheck, organizationKeyChallengeRequestCheck, organizationKeyProofClaims, parseContract, } from '@oxy.so/contracts/client';
|
|
2
2
|
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
3
3
|
import { evictOxyIdentityCache } from '../utils/identityCacheSweep.js';
|
|
4
4
|
import { evictOxyAccountForestCache, oxyAccountDetailCacheKey, OXY_ACCOUNT_PER_ACCOUNT_CACHE_PREFIX, } from '../utils/accountCacheSweep.js';
|
|
@@ -75,6 +75,72 @@ export class AccountAgentKeysApi {
|
|
|
75
75
|
return parseContract(agentKeyOperationResultCheck, response);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* `oxy.accounts.identityKey` — an organization's identity key (ADR 0061): its
|
|
80
|
+
* DID `#key-1`, the key Peable derives its FairCoin receive addresses from.
|
|
81
|
+
* Only the organization's owners and admins may read or change it; the key is
|
|
82
|
+
* created and held in Commons and never signs anybody in.
|
|
83
|
+
*
|
|
84
|
+
* `get` answers `publicKey: null` for an organization that has no key yet (its
|
|
85
|
+
* DID lists no `#key-1`): that is when a client shows its owners "Set up the
|
|
86
|
+
* organization's Peable wallet". A 403 (`ORGANIZATION_KEY_FORBIDDEN`) means the
|
|
87
|
+
* signed-in person is not an owner or admin of a claimed, active organization.
|
|
88
|
+
*/
|
|
89
|
+
export class AccountIdentityKeyApi {
|
|
90
|
+
constructor(ctx) {
|
|
91
|
+
this.ctx = ctx;
|
|
92
|
+
}
|
|
93
|
+
/** The organization's key (`null` without one) and every key it has had, oldest first. */
|
|
94
|
+
async get(accountId) {
|
|
95
|
+
return parseContract(organizationIdentityKeyStateCheck, await this.ctx.request('GET', `/accounts/${enc(accountId)}/identity-key`, undefined, { cache: false }));
|
|
96
|
+
}
|
|
97
|
+
/** A one-use challenge for one change, with the claims every signer signs. */
|
|
98
|
+
async requestChallenge(accountId, request) {
|
|
99
|
+
return parseContract(organizationKeyChallengeCheck, await this.ctx.request('POST', `/accounts/${enc(accountId)}/identity-key/challenge`, parseContract(organizationKeyChallengeRequestCheck, request), { cache: false }));
|
|
100
|
+
}
|
|
101
|
+
/** Apply a change with its signatures (see {@link link} for the whole ceremony). */
|
|
102
|
+
async apply(accountId, request) {
|
|
103
|
+
return parseContract(organizationIdentityKeyStateCheck, await this.ctx.request('POST', `/accounts/${enc(accountId)}/identity-key`, parseContract(organizationKeyApplyRequestCheck, request), { cache: false }));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Commons' whole ceremony. NATIVE-ONLY: the actor is THIS device's personal
|
|
107
|
+
* root (`KeyManager`), which must be the signed-in person's. `key` is the
|
|
108
|
+
* organization key the device generated (`create`, or `replace` with a new
|
|
109
|
+
* one); `previousKey` is the current organization key when the device holds
|
|
110
|
+
* it, so the replacement is recorded as proven by it.
|
|
111
|
+
*/
|
|
112
|
+
async link(accountId, input) {
|
|
113
|
+
const { KeyManager, signIdentityProof } = await import('../crypto/index.js');
|
|
114
|
+
const [rootPrivateKey, rootPublicKey] = await Promise.all([
|
|
115
|
+
KeyManager.getPrivateKey(),
|
|
116
|
+
KeyManager.getPublicKey(),
|
|
117
|
+
]);
|
|
118
|
+
if (!rootPrivateKey || !rootPublicKey) {
|
|
119
|
+
throw new Error('No identity on this device to sign with');
|
|
120
|
+
}
|
|
121
|
+
const challenge = await this.requestChallenge(accountId, {
|
|
122
|
+
operation: input.operation,
|
|
123
|
+
publicKey: input.key.publicKey,
|
|
124
|
+
});
|
|
125
|
+
const root = rootPublicKey.trim().toLowerCase();
|
|
126
|
+
if (challenge.actorPublicKey !== root) {
|
|
127
|
+
throw new Error("This device's identity is not the signed-in account's");
|
|
128
|
+
}
|
|
129
|
+
const sign = async (signer, publicKey) => (await signIdentityProof({ privateKey: signer.privateKey, publicKey }, organizationKeyProofClaims(challenge, publicKey))).signature;
|
|
130
|
+
const previousPublicKey = challenge.previousPublicKey;
|
|
131
|
+
return this.apply(accountId, {
|
|
132
|
+
operation: input.operation,
|
|
133
|
+
publicKey: challenge.publicKey,
|
|
134
|
+
challenge: challenge.challenge,
|
|
135
|
+
expiresAt: challenge.expiresAt,
|
|
136
|
+
actorSignature: await sign({ privateKey: rootPrivateKey, publicKey: root }, root),
|
|
137
|
+
keySignature: await sign(input.key, challenge.publicKey),
|
|
138
|
+
...(input.previousKey && previousPublicKey
|
|
139
|
+
? { previousKeySignature: await sign(input.previousKey, previousPublicKey) }
|
|
140
|
+
: {}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
78
144
|
/** Consent-based invitations; never cached or queued as offline mutations. */
|
|
79
145
|
export class AccountInvitationsApi {
|
|
80
146
|
constructor(ctx) {
|
|
@@ -109,6 +175,7 @@ export class AccountsApi {
|
|
|
109
175
|
this.members = new AccountMembersApi(ctx);
|
|
110
176
|
this.invitations = new AccountInvitationsApi(ctx);
|
|
111
177
|
this.agentKeys = new AccountAgentKeysApi(ctx);
|
|
178
|
+
this.identityKey = new AccountIdentityKeyApi(ctx);
|
|
112
179
|
}
|
|
113
180
|
/**
|
|
114
181
|
* List the accounts the caller can access: their own personal (root)
|
|
@@ -18,7 +18,7 @@ import type { AccountAccessGrant, OrganizationType } from '@oxy.so/contracts';
|
|
|
18
18
|
* no per-request "acting-as" header. Identity is carried by the session/token,
|
|
19
19
|
* so a switch propagates through reload and cross-domain exactly like a login.
|
|
20
20
|
*/
|
|
21
|
-
import type { AgentKeyOperation, AgentKeyOperationProof } from '@oxy.so/contracts';
|
|
21
|
+
import type { AgentKeyOperation, AgentKeyOperationProof, OrganizationIdentityKeyState, OrganizationKeyApplyRequest, OrganizationKeyChallenge, OrganizationKeyChallengeRequest, OrganizationKeyOperation } from '@oxy.so/contracts';
|
|
22
22
|
import type { User } from '../models/interfaces';
|
|
23
23
|
import type { AccountCategoryId, AccountKind, AvatarCharacter, ChildAccountKind } from '@oxy.so/contracts';
|
|
24
24
|
import type { SessionLoginResponse } from '../models/session';
|
|
@@ -384,6 +384,44 @@ export declare class AccountAgentKeysApi {
|
|
|
384
384
|
revokedMethodIds: string[];
|
|
385
385
|
}>;
|
|
386
386
|
}
|
|
387
|
+
/** A secp256k1 key pair held on this device: lowercase hex, the public key uncompressed. */
|
|
388
|
+
export interface OrganizationKeyPair {
|
|
389
|
+
privateKey: string;
|
|
390
|
+
publicKey: string;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* `oxy.accounts.identityKey` — an organization's identity key (ADR 0061): its
|
|
394
|
+
* DID `#key-1`, the key Peable derives its FairCoin receive addresses from.
|
|
395
|
+
* Only the organization's owners and admins may read or change it; the key is
|
|
396
|
+
* created and held in Commons and never signs anybody in.
|
|
397
|
+
*
|
|
398
|
+
* `get` answers `publicKey: null` for an organization that has no key yet (its
|
|
399
|
+
* DID lists no `#key-1`): that is when a client shows its owners "Set up the
|
|
400
|
+
* organization's Peable wallet". A 403 (`ORGANIZATION_KEY_FORBIDDEN`) means the
|
|
401
|
+
* signed-in person is not an owner or admin of a claimed, active organization.
|
|
402
|
+
*/
|
|
403
|
+
export declare class AccountIdentityKeyApi {
|
|
404
|
+
private readonly ctx;
|
|
405
|
+
constructor(ctx: OxyContext);
|
|
406
|
+
/** The organization's key (`null` without one) and every key it has had, oldest first. */
|
|
407
|
+
get(accountId: string): Promise<OrganizationIdentityKeyState>;
|
|
408
|
+
/** A one-use challenge for one change, with the claims every signer signs. */
|
|
409
|
+
requestChallenge(accountId: string, request: OrganizationKeyChallengeRequest): Promise<OrganizationKeyChallenge>;
|
|
410
|
+
/** Apply a change with its signatures (see {@link link} for the whole ceremony). */
|
|
411
|
+
apply(accountId: string, request: OrganizationKeyApplyRequest): Promise<OrganizationIdentityKeyState>;
|
|
412
|
+
/**
|
|
413
|
+
* Commons' whole ceremony. NATIVE-ONLY: the actor is THIS device's personal
|
|
414
|
+
* root (`KeyManager`), which must be the signed-in person's. `key` is the
|
|
415
|
+
* organization key the device generated (`create`, or `replace` with a new
|
|
416
|
+
* one); `previousKey` is the current organization key when the device holds
|
|
417
|
+
* it, so the replacement is recorded as proven by it.
|
|
418
|
+
*/
|
|
419
|
+
link(accountId: string, input: {
|
|
420
|
+
operation: OrganizationKeyOperation;
|
|
421
|
+
key: OrganizationKeyPair;
|
|
422
|
+
previousKey?: OrganizationKeyPair;
|
|
423
|
+
}): Promise<OrganizationIdentityKeyState>;
|
|
424
|
+
}
|
|
387
425
|
export interface AccountInvitation {
|
|
388
426
|
id: string;
|
|
389
427
|
accountId: string;
|
|
@@ -425,6 +463,8 @@ export declare class AccountsApi {
|
|
|
425
463
|
readonly members: AccountMembersApi;
|
|
426
464
|
readonly invitations: AccountInvitationsApi;
|
|
427
465
|
readonly agentKeys: AccountAgentKeysApi;
|
|
466
|
+
/** An organization's identity key: its DID `#key-1` and Peable wallet key (ADR 0061). */
|
|
467
|
+
readonly identityKey: AccountIdentityKeyApi;
|
|
428
468
|
constructor(ctx: OxyContext);
|
|
429
469
|
/**
|
|
430
470
|
* List the accounts the caller can access: their own personal (root)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.24.0",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"dependencies": {
|
|
182
182
|
"@noble/ciphers": "^2.4.0",
|
|
183
183
|
"@noble/hashes": "^2.4.0",
|
|
184
|
-
"@oxy.so/contracts": "^6.
|
|
184
|
+
"@oxy.so/contracts": "^6.11.0",
|
|
185
185
|
"@oxy.so/protocol": "^1.6.1",
|
|
186
186
|
"@oxy.so/telemetry": "^2.1.0",
|
|
187
187
|
"@scure/bip39": "^2.4.0",
|
package/src/api/accounts.ts
CHANGED
|
@@ -18,13 +18,26 @@ import type { AccountAccessGrant, OrganizationType } from '@oxy.so/contracts';
|
|
|
18
18
|
* no per-request "acting-as" header. Identity is carried by the session/token,
|
|
19
19
|
* so a switch propagates through reload and cross-domain exactly like a login.
|
|
20
20
|
*/
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
AgentKeyOperation,
|
|
23
|
+
AgentKeyOperationProof,
|
|
24
|
+
OrganizationIdentityKeyState,
|
|
25
|
+
OrganizationKeyApplyRequest,
|
|
26
|
+
OrganizationKeyChallenge,
|
|
27
|
+
OrganizationKeyChallengeRequest,
|
|
28
|
+
OrganizationKeyOperation,
|
|
29
|
+
} from '@oxy.so/contracts';
|
|
22
30
|
import {
|
|
23
31
|
agentKeyListResponseCheck,
|
|
24
32
|
agentKeyOperationChallengeCheck,
|
|
25
33
|
agentKeyOperationCheck,
|
|
26
34
|
agentKeyOperationResultCheck,
|
|
27
35
|
executeAgentKeyOperationCheck,
|
|
36
|
+
organizationIdentityKeyStateCheck,
|
|
37
|
+
organizationKeyApplyRequestCheck,
|
|
38
|
+
organizationKeyChallengeCheck,
|
|
39
|
+
organizationKeyChallengeRequestCheck,
|
|
40
|
+
organizationKeyProofClaims,
|
|
28
41
|
parseContract,
|
|
29
42
|
} from '@oxy.so/contracts/client';
|
|
30
43
|
import type { User } from '../models/interfaces';
|
|
@@ -457,6 +470,124 @@ export class AccountAgentKeysApi {
|
|
|
457
470
|
}
|
|
458
471
|
}
|
|
459
472
|
|
|
473
|
+
/** A secp256k1 key pair held on this device: lowercase hex, the public key uncompressed. */
|
|
474
|
+
export interface OrganizationKeyPair {
|
|
475
|
+
privateKey: string;
|
|
476
|
+
publicKey: string;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* `oxy.accounts.identityKey` — an organization's identity key (ADR 0061): its
|
|
481
|
+
* DID `#key-1`, the key Peable derives its FairCoin receive addresses from.
|
|
482
|
+
* Only the organization's owners and admins may read or change it; the key is
|
|
483
|
+
* created and held in Commons and never signs anybody in.
|
|
484
|
+
*
|
|
485
|
+
* `get` answers `publicKey: null` for an organization that has no key yet (its
|
|
486
|
+
* DID lists no `#key-1`): that is when a client shows its owners "Set up the
|
|
487
|
+
* organization's Peable wallet". A 403 (`ORGANIZATION_KEY_FORBIDDEN`) means the
|
|
488
|
+
* signed-in person is not an owner or admin of a claimed, active organization.
|
|
489
|
+
*/
|
|
490
|
+
export class AccountIdentityKeyApi {
|
|
491
|
+
constructor(private readonly ctx: OxyContext) {}
|
|
492
|
+
|
|
493
|
+
/** The organization's key (`null` without one) and every key it has had, oldest first. */
|
|
494
|
+
async get(accountId: string): Promise<OrganizationIdentityKeyState> {
|
|
495
|
+
return parseContract(
|
|
496
|
+
organizationIdentityKeyStateCheck,
|
|
497
|
+
await this.ctx.request<unknown>(
|
|
498
|
+
'GET',
|
|
499
|
+
`/accounts/${enc(accountId)}/identity-key`,
|
|
500
|
+
undefined,
|
|
501
|
+
{ cache: false },
|
|
502
|
+
),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** A one-use challenge for one change, with the claims every signer signs. */
|
|
507
|
+
async requestChallenge(
|
|
508
|
+
accountId: string,
|
|
509
|
+
request: OrganizationKeyChallengeRequest,
|
|
510
|
+
): Promise<OrganizationKeyChallenge> {
|
|
511
|
+
return parseContract(
|
|
512
|
+
organizationKeyChallengeCheck,
|
|
513
|
+
await this.ctx.request<unknown>(
|
|
514
|
+
'POST',
|
|
515
|
+
`/accounts/${enc(accountId)}/identity-key/challenge`,
|
|
516
|
+
parseContract(organizationKeyChallengeRequestCheck, request),
|
|
517
|
+
{ cache: false },
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Apply a change with its signatures (see {@link link} for the whole ceremony). */
|
|
523
|
+
async apply(
|
|
524
|
+
accountId: string,
|
|
525
|
+
request: OrganizationKeyApplyRequest,
|
|
526
|
+
): Promise<OrganizationIdentityKeyState> {
|
|
527
|
+
return parseContract(
|
|
528
|
+
organizationIdentityKeyStateCheck,
|
|
529
|
+
await this.ctx.request<unknown>(
|
|
530
|
+
'POST',
|
|
531
|
+
`/accounts/${enc(accountId)}/identity-key`,
|
|
532
|
+
parseContract(organizationKeyApplyRequestCheck, request),
|
|
533
|
+
{ cache: false },
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Commons' whole ceremony. NATIVE-ONLY: the actor is THIS device's personal
|
|
540
|
+
* root (`KeyManager`), which must be the signed-in person's. `key` is the
|
|
541
|
+
* organization key the device generated (`create`, or `replace` with a new
|
|
542
|
+
* one); `previousKey` is the current organization key when the device holds
|
|
543
|
+
* it, so the replacement is recorded as proven by it.
|
|
544
|
+
*/
|
|
545
|
+
async link(
|
|
546
|
+
accountId: string,
|
|
547
|
+
input: {
|
|
548
|
+
operation: OrganizationKeyOperation;
|
|
549
|
+
key: OrganizationKeyPair;
|
|
550
|
+
previousKey?: OrganizationKeyPair;
|
|
551
|
+
},
|
|
552
|
+
): Promise<OrganizationIdentityKeyState> {
|
|
553
|
+
const { KeyManager, signIdentityProof } = await import('../crypto/index');
|
|
554
|
+
const [rootPrivateKey, rootPublicKey] = await Promise.all([
|
|
555
|
+
KeyManager.getPrivateKey(),
|
|
556
|
+
KeyManager.getPublicKey(),
|
|
557
|
+
]);
|
|
558
|
+
if (!rootPrivateKey || !rootPublicKey) {
|
|
559
|
+
throw new Error('No identity on this device to sign with');
|
|
560
|
+
}
|
|
561
|
+
const challenge = await this.requestChallenge(accountId, {
|
|
562
|
+
operation: input.operation,
|
|
563
|
+
publicKey: input.key.publicKey,
|
|
564
|
+
});
|
|
565
|
+
const root = rootPublicKey.trim().toLowerCase();
|
|
566
|
+
if (challenge.actorPublicKey !== root) {
|
|
567
|
+
throw new Error("This device's identity is not the signed-in account's");
|
|
568
|
+
}
|
|
569
|
+
const sign = async (signer: OrganizationKeyPair, publicKey: string) =>
|
|
570
|
+
(
|
|
571
|
+
await signIdentityProof(
|
|
572
|
+
{ privateKey: signer.privateKey, publicKey },
|
|
573
|
+
organizationKeyProofClaims(challenge, publicKey),
|
|
574
|
+
)
|
|
575
|
+
).signature;
|
|
576
|
+
const previousPublicKey = challenge.previousPublicKey;
|
|
577
|
+
return this.apply(accountId, {
|
|
578
|
+
operation: input.operation,
|
|
579
|
+
publicKey: challenge.publicKey,
|
|
580
|
+
challenge: challenge.challenge,
|
|
581
|
+
expiresAt: challenge.expiresAt,
|
|
582
|
+
actorSignature: await sign({ privateKey: rootPrivateKey, publicKey: root }, root),
|
|
583
|
+
keySignature: await sign(input.key, challenge.publicKey),
|
|
584
|
+
...(input.previousKey && previousPublicKey
|
|
585
|
+
? { previousKeySignature: await sign(input.previousKey, previousPublicKey) }
|
|
586
|
+
: {}),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
460
591
|
export interface AccountInvitation {
|
|
461
592
|
id: string;
|
|
462
593
|
accountId: string;
|
|
@@ -540,11 +671,14 @@ export class AccountsApi {
|
|
|
540
671
|
readonly members: AccountMembersApi;
|
|
541
672
|
readonly invitations: AccountInvitationsApi;
|
|
542
673
|
readonly agentKeys: AccountAgentKeysApi;
|
|
674
|
+
/** An organization's identity key: its DID `#key-1` and Peable wallet key (ADR 0061). */
|
|
675
|
+
readonly identityKey: AccountIdentityKeyApi;
|
|
543
676
|
|
|
544
677
|
constructor(private readonly ctx: OxyContext) {
|
|
545
678
|
this.members = new AccountMembersApi(ctx);
|
|
546
679
|
this.invitations = new AccountInvitationsApi(ctx);
|
|
547
680
|
this.agentKeys = new AccountAgentKeysApi(ctx);
|
|
681
|
+
this.identityKey = new AccountIdentityKeyApi(ctx);
|
|
548
682
|
}
|
|
549
683
|
|
|
550
684
|
/**
|