Skip to content

Commit

Permalink
Adds Credentials Module (#150)
Browse files Browse the repository at this point in the history
* Add credentials module

* Adds changelog

* Adds changelog

* Make private storage property mandatory
  • Loading branch information
Juliano1612 authored Jul 27, 2023
1 parent cab5a7e commit c942c3c
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 2 deletions.
19 changes: 19 additions & 0 deletions .changeset/chatty-doors-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@spruceid/ssx': minor
---

This adds the Credentials Modules, which allows developers to fetch credentials issued on SpruceKit Credential Issuer.
This module requires Storage Module, so you must enable both to make it work.

```ts
const ssx = SSX({
modules: {
storage: true,
credentials: true
}
})

await ssx.signIn();

const { data } = ssx.credentials.list();
```
18 changes: 18 additions & 0 deletions examples/ssx-test-app/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function Home() {
const [statement, setStatement] = useState<string>('');
// ssx module config
const [storageEnabled, setStorageEnabled] = useState<string>('Off');
const [credentialsEnabled, setCredentialsEnabled] = useState('Off');

const getSSXConfig = (ssxConfig: Record<string, any> = {}) => {
if (server === 'On') {
Expand Down Expand Up @@ -112,6 +113,10 @@ function Home() {
modules.storage = true;
}

if (credentialsEnabled === "On") {
modules.credentials = true;
}

if (modules) {
ssxConfig = {
...ssxConfig,
Expand Down Expand Up @@ -315,6 +320,19 @@ function Home() {
/>
</div>
</div>
<div className='Dropdown-item'>
<span className='Dropdown-item-name'>
Credentials
</span>
<div className='Dropdown-item-options'>
<RadioGroup
name='credentialsEnabled'
options={['On', 'Off']}
value={credentialsEnabled}
onChange={setCredentialsEnabled}
/>
</div>
</div>
</Dropdown>
{
server === 'On' ?
Expand Down
34 changes: 33 additions & 1 deletion examples/ssx-test-app/src/pages/StorageModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ interface IStorageModule {

function StorageModule({ ssx }: IStorageModule) {
const [contentList, setContentList] = useState<Array<string>>([]);
const [credentialsList, setCredentialsList] = useState<Array<string>>([]);
const [selectedContent, setSelectedContent] = useState<string | null>(null);
const [name, setName] = useState<string>('');
const [text, setText] = useState<string>('');
const [viewingList, setViewingList] = useState<boolean>(true);
const [allowPost, setAllowPost] = useState<boolean>(false);

useEffect(() => {
const getContentList = async () => {
const { data } = await ssx.storage.list({ removePrefix: true });
setContentList(data);
};
const getCredentialList = async () => {
const { data } = await ssx.credentials?.list?.({ removePrefix: true });
setCredentialsList(data);
};
getContentList();
getCredentialList();
}, [ssx]);

const handleShareContent = async (content: string) => {
Expand All @@ -33,6 +40,7 @@ function StorageModule({ ssx }: IStorageModule) {

const handleGetContent = async (content: string) => {
const { data } = await ssx.storage.get(content);
setAllowPost(true);
setSelectedContent(content);
setName(content);
setText(data);
Expand Down Expand Up @@ -75,6 +83,15 @@ function StorageModule({ ssx }: IStorageModule) {
setViewingList(false);
};

const handleGetCredential = async (content: string) => {
const { data } = await ssx.credentials.get(content);
setAllowPost(false);
setSelectedContent(content);
setName(content);
setText(data);
setViewingList(false);
};

return (
<div className="Content" style={{ marginTop: '30px' }}>
<div className="storage-container Content-container">
Expand Down Expand Up @@ -102,13 +119,28 @@ function StorageModule({ ssx }: IStorageModule) {
</div>
))}
<Button onClick={handlePostNewContent}>Post new content</Button>
<h3>Credentials List</h3>
{credentialsList.map(content => (
<div className="item-container" key={content}>
<span>{content}</span>
<Button
className="smallButton"
onClick={() => handleGetCredential(content)}>
Get
</Button>
</div>
))}
</div>
) : (
<div className="View-pane">
<h3>View/Edit/Post Pane</h3>
<Input label="Key" value={name} onChange={setName} />
<Input label="Text" value={text} onChange={setText} />
<Button onClick={handlePostContent}>Post</Button>
{
allowPost ?
<Button onClick={handlePostContent}>Post</Button> :
null
}
<Button onClick={() => setViewingList(true)}>Back</Button>
</div>
)}
Expand Down
49 changes: 49 additions & 0 deletions packages/ssx-sdk/src/modules/Credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { SSXExtension } from "@spruceid/ssx-core/client";
import { IStorage } from "./Storage";
import { Response, Request } from './Storage/kepler';


export interface ICredentialsList {
credentialType?: string,
request?: Request,
removePrefix?: boolean
}

export interface ICredentials extends SSXExtension {
/**
* Retrieves the stored value associated with the specified credential key.
* @param credential - The unique identifier for the stored value.
* @returns A Promise that resolves to the value associated with the given credential key or undefined if the credential key does not exist.
*/
get(credential: string): Promise<Response>;

/**
* Lists all credential keys available.
* @param credentialType - The credential identifier for the stored value.
* @returns A Promise that resolves to an array of strings representing the stored credentials keys.
*/
list(options: ICredentialsList): Promise<Response>;

}
export class Credentials implements ICredentials {
public namespace = 'credentials';
private prefix: string = 'shared/credentials';
private storage: IStorage;

constructor(storage: IStorage) {
this.storage = storage;
}

public async get(credential: string): Promise<Response> {
return this.storage.get(credential, { prefix: this.prefix });
}

public async list(options: ICredentialsList): Promise<Response> {
const defaultOptions = {
prefix: this.prefix,
};
const { prefix, credentialType, request, removePrefix } = { ...defaultOptions, ...options };
return this.storage.list({ prefix, path: credentialType, removePrefix, request });
}

}
9 changes: 9 additions & 0 deletions packages/ssx-sdk/src/modules/Storage/KeplerStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class KeplerStorage implements IStorage, IKepler {
private autoCreateNewOrbit: boolean;
private userAuth: IUserAuthorization;
private keplerModule?: any;
private credentialsModule?: boolean;
/** The users orbitId. */
public orbitId?: string;

Expand All @@ -65,6 +66,7 @@ export class KeplerStorage implements IStorage, IKepler {
this.userAuth = userAuth;
this.hosts = [...(config?.hosts || []), 'https://kepler.spruceid.xyz'];
this.prefix = config?.prefix || '';
this.credentialsModule = config?.credentialsModule;
this.autoCreateNewOrbit =
config?.autoCreateNewOrbit === undefined
? true
Expand Down Expand Up @@ -98,6 +100,13 @@ export class KeplerStorage implements IStorage, IKepler {
'del',
'metadata',
];
if (this.credentialsModule) {
actions[`${this.orbitId}/kv/shared/credentials`] = [
'get',
'list',
'metadata',
];
}
return actions;
}

Expand Down
1 change: 1 addition & 0 deletions packages/ssx-sdk/src/modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './UserAuthorization';
export * from './Storage';
export * from './Credentials';
24 changes: 23 additions & 1 deletion packages/ssx-sdk/src/ssx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
SSXLensProfilesResponse,
} from '@spruceid/ssx-core';
import {
Credentials,
ICredentials,
IUserAuthorization,
KeplerStorage,
UserAuthorization,
Expand All @@ -27,6 +29,7 @@ declare global {
*/
interface SSXModuleConfig {
storage?: boolean | { [key: string]: any };
credentials?: boolean;
}

// temporary: will move to ssx-core
Expand Down Expand Up @@ -68,30 +71,49 @@ export class SSX {
/** Storage Module */
public storage: KeplerStorage;

/** Credentials Module */
public credentials: ICredentials;

constructor(private config: SSXConfig = SSX_DEFAULT_CONFIG) {
// TODO: pull out config validation into separate function
// TODO: pull out userAuthorization config
this.userAuthorization = new UserAuthorization(config);

// initialize storage module
// assume credentials is **disabled** if config.credentials is not defined
const credentialsConfig =
config?.modules?.credentials === undefined ? false : config.modules.credentials;

// assume storage module is **disabled** if config.storage is not defined
const storageConfig =
config?.modules?.storage === undefined ? false : config.modules.storage;

if (storageConfig !== false) {
if (typeof storageConfig === 'object') {
storageConfig.credentialsModule = credentialsConfig;
// Initialize storage with the provided config
this.storage = new KeplerStorage(storageConfig, this.userAuthorization);
} else {
// storage == true or undefined
// Initialize storage with default config when no other condition is met
this.storage = new KeplerStorage(
{ prefix: 'ssx' },
{ prefix: 'ssx', credentialsModule: credentialsConfig },
this.userAuthorization
);
}
this.extend(this.storage);
}

if (credentialsConfig) {
// Credentials module depends on the storage module. If it isn't enabled
// we won't initialize the credentials module.
if (!storageConfig) {
throw new Error('You must enable the storage module to use the credentials module.')
} else {
this.credentials = new Credentials(this.storage);
this.extend(this.credentials);
}
}
}

/**
Expand Down

0 comments on commit c942c3c

Please sign in to comment.