Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
Signed-off-by: pstlouis <[email protected]>
  • Loading branch information
PatStLouis committed Jan 3, 2025
1 parent 8fe3bce commit 98d1dc3
Show file tree
Hide file tree
Showing 12 changed files with 658 additions and 1 deletion.
26 changes: 26 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# For details on how this file works refer to:
# - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
# Maintain dependencies for GitHub Actions
# - Check for updates once a week
# - Group all updates into a single PR
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
all-actions:
patterns: ["*"]

# Maintain pip dependencies
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
groups:
dev:
dependency-type: development
minor:
dependency-type: production
update-types: [minor, patch]
41 changes: 41 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Checks

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
format:
name: Format and Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: psf/[email protected]
with:
src: "./vcdm"
- uses: chartboost/ruff-action@v1
with:
version: 0.3.4

test:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]

steps:
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: poetry
- name: Install dependencies
run: poetry install
- name: Run pytest
run: poetry run pytest
30 changes: 30 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Upload Python Package to PyPI when a Release is Created

on:
release:
types: [created]

jobs:
pypi-publish:
name: Publish release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/oca-cli
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
- name: Build package
run: |
poetry build -f sdist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Sample files
sample*

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# oca-cli
# oca-cli

## Quickstart

```bash
# install the package
pip install oca-cli

# Create a sample schema file
SAMPLE_BUNDLE=$(cat <<EOF
{
"name": "Sample",
"description": "A sample bundle",
"issuer": "Demo issuer",
"attributes": [
"first_name",
"last_name"
]
}
EOF
)
echo $SAMPLE_BUNDLE > sample_schema.json

# Draft an OCA Bundle
oca draft -f sample_schema.json > sample_draft.json

# Edit the bundle then secure it
oca secure -f sample_draft.json > sample_bundle.json

```
Empty file added oca/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions oca/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import typer
import json
from typing_extensions import Annotated
from oca.processor import OCAProcessor

cli = typer.Typer()

@cli.command()
def draft(
file: Annotated[str, typer.Option(
'-f', '--file',
help="Schema file name.",
prompt=True
)] = 'samples/schema.json'
):
"""Draft an OCA Bundle.
"""
with open(file, 'r') as f:
schema = json.loads(f.read())

drafted_bundle = OCAProcessor().draft_bundle(schema)
print(json.dumps(drafted_bundle, indent=2))

@cli.command()
def secure(
file: Annotated[str, typer.Option(
'-f', '--file',
help="OCA Bundle file name.",
prompt=True
)] = 'samples/bundle.json'
):
"""Secure an OCA Bundle.
"""
with open(file, 'r') as f:
bundle = json.loads(f.read())

secured_bundle = OCAProcessor().secure_bundle(bundle)
print(json.dumps(secured_bundle, indent=2))

if __name__ == "__main__":
typer.run(cli)
169 changes: 169 additions & 0 deletions oca/processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import base64
from blake3 import blake3
import jcs
import json
import re


class OCAProcessorError(Exception):
"""Generic OCAProcessor Error."""


class OCAProcessor:
def __init__(self):
self.dummy_string = "#" * 44

def generate_said(self, value):
# https://datatracker.ietf.org/doc/html/draft-ssmith-said#name-generation-and-verification
# https://trustoverip.github.io/tswg-cesr-specification/#text-coding-scheme-design
return "E" + (
base64.urlsafe_b64encode(
bytes([0]) + blake3(jcs.canonicalize(value)).digest()
).decode()
).lstrip("A")

def secure_bundle(self, bundle):
capture_base = bundle[0]
overlays = capture_base.pop('overlays')

capture_base["digest"] = self.dummy_string
capture_base["digest"] = self.generate_said(capture_base)

for idx, overlay in enumerate(overlays):
overlays[idx]["capture_base"] = capture_base["digest"]
overlays[idx]["digest"] = self.dummy_string
overlays[idx]["digest"] = self.generate_said(overlays[idx])

secures_bundle = [capture_base | {"overlays": overlays}]
return secures_bundle

def draft_bundle(self, schema):
capture_base = {
"type": "spec/capture_base/1.0",
"attributes": {attribute: 'Text' for attribute in schema['attributes']},
"flagged_attributes": [],
'overlays': []
}
encoding = {
"type": "spec/overlays/character_encoding/1.0",
"default_character_encoding": "utf-8",
"attribute_character_encoding": {attribute: "utf-8" for attribute in schema['attributes']},
}
capture_base['overlays'].append(encoding)

labels = {
"type": "spec/overlays/label/1.0",
"lang": "en",
"attribute_labels": {attribute: attribute.replace('_', ' ').capitalize() for attribute in schema['attributes']},
}
capture_base['overlays'].append(labels)

information = {
"type": "spec/overlays/information/1.0",
"lang": "en",
"attribute_information": {attribute: 'Lorem ipsum' for attribute in schema['attributes']},
}
capture_base['overlays'].append(information)

meta = {
"type": "spec/overlays/meta/1.0",
"language": "en",
"issuer": schema["name"],
"name": schema["name"],
"description": schema['description'],
"credential_help_text": "Learn more",
"credential_support_url": ""
}
capture_base['overlays'].append(meta)

branding = {
"type": "aries/overlays/branding/1.0",
"logo": "",
"background_image": "",
"background_image_slice": "",
"primary_background_color": "",
"secondary_background_color": "",
"primary_attribute": "",
"secondary_attribute": "",
"expiry_date_attribute": "",
}
capture_base['overlays'].append(branding)
return [capture_base]

def create_bundle(self, credential_registration, credential_template):
capture_base = {
"type": "spec/capture_base/1.0",
"attributes": {},
"flagged_attributes": [],
"digest": self.dummy_string,
}
labels = {
"type": "spec/overlays/label/1.0",
"lang": "en",
"attribute_labels": {},
}
information = {
"type": "spec/overlays/information/1.0",
"lang": "en",
"attribute_information": {},
}
meta = {
"type": "spec/overlays/meta/1.0",
"language": "en",
"issuer": credential_template["issuer"]["name"],
"name": credential_template["name"],
# "description": credential_template['description'],
}

branding = {
"type": "aries/overlays/branding/1.0",
"primary_attribute": "entityId",
"secondary_attribute": "cardinalityId",
"primary_background_color": "#003366",
"secondary_background_color": "#00264D",
"logo": "https://avatars.githubusercontent.com/u/916280",
}
paths = {"type": "vc/overlays/path/1.0", "attribute_paths": {}}
clusters = {
"type": "vc/overlays/cluster/1.0",
"lang": "en",
"attribute_clusters": {},
}
attributes = (
credential_registration["corePaths"]
| credential_registration["subjectPaths"]
)
for attribute in attributes:
capture_base["attributes"][attribute] = "Text"
labels["attribute_labels"][attribute] = " ".join(
re.findall("[A-Z][^A-Z]*", attribute)
).upper()
paths["attribute_paths"][attribute] = attributes[attribute]

overlays = [
labels,
# information,
meta,
branding,
paths,
# clusters,
]

capture_base["digest"] = self.generate_said(capture_base)
for idx, overlay in enumerate(overlays):
overlays[idx]["capture_base"] = capture_base["digest"]
overlays[idx]["digest"] = self.dummy_string
overlays[idx]["digest"] = self.generate_said(overlays[idx])

bundle = capture_base | {"overlays": overlays}
return bundle

def get_overlay(self, bundle, overlay_type):
return next(
(
overlay
for overlay in bundle["overlays"]
if overlay["type"] == overlay_type
),
None,
)
Loading

0 comments on commit 98d1dc3

Please sign in to comment.