Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
4 changes: 2 additions & 2 deletions README.md → README
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
```
PIERRE COMPUTER COMPANY █
PROJECT: SDKS

Expand All @@ -15,4 +14,5 @@ SDKS:
- Typescript
- Python
- Go
```

[SDK DOCUMENTATION](https://code.storage/docs/reference/sdk)
3 changes: 3 additions & 0 deletions packages/code-storage-go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ func (c *Client) generateJWT(repoID string, options RemoteURLOptions) (string, e
"iat": issuedAt.Unix(),
"exp": issuedAt.Add(ttl).Unix(),
}
if len(options.Ops) > 0 {
claims["ops"] = options.Ops
}

token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
return token.SignedString(c.privateKey)
Expand Down
36 changes: 36 additions & 0 deletions packages/code-storage-go/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,42 @@ func TestImportRemoteURL(t *testing.T) {
}
}

func TestRemoteURLOps(t *testing.T) {
client, err := NewClient(Options{Name: "acme", Key: testKey, StorageBaseURL: "acme.code.storage"})
if err != nil {
t.Fatalf("client error: %v", err)
}
repo := &Repo{ID: "repo-1", DefaultBranch: "main", client: client}

t.Run("includes ops in JWT when provided", func(t *testing.T) {
remote, err := repo.RemoteURL(nil, RemoteURLOptions{
Ops: Ops{OpNoForcePush},
})
if err != nil {
t.Fatalf("remote url error: %v", err)
}
claims := parseJWTFromURL(t, remote)
ops, ok := claims["ops"].([]interface{})
if !ok {
t.Fatalf("expected ops claim to be a list")
}
if len(ops) != 1 || ops[0] != "no-force-push" {
t.Fatalf("unexpected ops: %v", ops)
}
})

t.Run("omits ops from JWT when not provided", func(t *testing.T) {
remote, err := repo.RemoteURL(nil, RemoteURLOptions{})
if err != nil {
t.Fatalf("remote url error: %v", err)
}
claims := parseJWTFromURL(t, remote)
if _, ok := claims["ops"]; ok {
t.Fatalf("expected no ops claim")
}
})
}

func TestListFilesEphemeral(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/files" {
Expand Down
11 changes: 11 additions & 0 deletions packages/code-storage-go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,21 @@ type Options struct {
HTTPClient *http.Client
}

// Op is a policy operation included in the JWT.
type Op = string

const (
OpNoForcePush Op = "no-force-push"
)

// Ops is a list of policy operations.
type Ops []Op

// RemoteURLOptions configure token generation for remote URLs.
type RemoteURLOptions struct {
Permissions []Permission
TTL time.Duration
Ops Ops
}

// InvocationOptions holds common request options.
Expand Down
6 changes: 6 additions & 0 deletions packages/code-storage-python/pierre_storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
GetBranchDiffResult,
GetCommitDiffResult,
GitStorageOptions,
Op,
OP_NO_FORCE_PUSH,
Ops,
GrepFileMatch,
GrepLine,
GrepResult,
Expand Down Expand Up @@ -83,6 +86,9 @@
"GrepLine",
"GrepResult",
"GitStorageOptions",
"Op",
"OP_NO_FORCE_PUSH",
"Ops",
"ListBranchesResult",
"ListCommitsResult",
"ListFilesResult",
Expand Down
4 changes: 4 additions & 0 deletions packages/code-storage-python/pierre_storage/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def generate_jwt(
repo_id: str,
scopes: Optional[List[str]] = None,
ttl: int = 31536000, # 1 year default
ops: Optional[List[str]] = None,
) -> str:
"""Generate a JWT token for Git storage authentication.

Expand All @@ -22,6 +23,7 @@ def generate_jwt(
repo_id: Repository identifier
scopes: List of permission scopes (defaults to ['git:write', 'git:read'])
ttl: Time-to-live in seconds (defaults to 1 year)
ops: List of policy operations (e.g., ['no-force-push'])

Returns:
Signed JWT token string
Expand All @@ -41,6 +43,8 @@ def generate_jwt(
"iat": now,
"exp": now + ttl,
}
if ops:
payload["ops"] = ops

# Load the private key and determine algorithm
try:
Expand Down
6 changes: 5 additions & 1 deletion packages/code-storage-python/pierre_storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import uuid
from datetime import datetime, timezone
from typing import Any, Dict, Optional, Union, cast
from typing import Any, Dict, List, Optional, Union, cast
from urllib.parse import urlencode

import httpx
Expand Down Expand Up @@ -608,6 +608,7 @@ def _generate_jwt(
"""
permissions = ["git:write", "git:read"]
ttl: int = 31536000 # 1 year default
ops: Optional[List[str]] = None

if options:
if "permissions" in options:
Expand All @@ -616,6 +617,8 @@ def _generate_jwt(
option_ttl = options["ttl"]
if isinstance(option_ttl, int):
ttl = option_ttl
if "ops" in options:
ops = options["ops"]
elif "default_ttl" in self.options:
default_ttl = self.options["default_ttl"]
if isinstance(default_ttl, int):
Expand All @@ -627,6 +630,7 @@ def _generate_jwt(
repo_id,
permissions,
ttl,
ops,
)


Expand Down
12 changes: 10 additions & 2 deletions packages/code-storage-python/pierre_storage/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,14 @@ async def get_remote_url(
*,
permissions: Optional[list[str]] = None,
ttl: Optional[int] = None,
ops: Optional[list[str]] = None,
) -> str:
"""Get remote URL for Git operations.

Args:
permissions: List of permissions (e.g., ["git:write", "git:read"])
ttl: Token TTL in seconds
ops: List of policy operations (e.g., ["no-force-push"])

Returns:
Git remote URL with embedded JWT
Expand All @@ -182,6 +184,8 @@ async def get_remote_url(
options["permissions"] = permissions
if ttl is not None:
options["ttl"] = ttl
if ops is not None:
options["ops"] = ops

jwt_token = self.generate_jwt(self._id, options if options else None)
url = f"https://t:{jwt_token}@{self.storage_base_url}/{self._id}.git"
Expand All @@ -192,35 +196,39 @@ async def get_import_remote_url(
*,
permissions: Optional[list[str]] = None,
ttl: Optional[int] = None,
ops: Optional[list[str]] = None,
) -> str:
"""Get import remote URL for Git operations.

Args:
permissions: List of permissions (e.g., ["git:write", "git:read"])
ttl: Token TTL in seconds
ops: List of policy operations (e.g., ["no-force-push"])

Returns:
Git remote URL with embedded JWT pointing to import namespace
"""
url = await self.get_remote_url(permissions=permissions, ttl=ttl)
url = await self.get_remote_url(permissions=permissions, ttl=ttl, ops=ops)
return url.replace(".git", "+import.git")

async def get_ephemeral_remote_url(
self,
*,
permissions: Optional[list[str]] = None,
ttl: Optional[int] = None,
ops: Optional[list[str]] = None,
) -> str:
"""Get ephemeral remote URL for Git operations.

Args:
permissions: List of permissions (e.g., ["git:write", "git:read"])
ttl: Token TTL in seconds
ops: List of policy operations (e.g., ["no-force-push"])

Returns:
Git remote URL with embedded JWT pointing to ephemeral namespace
"""
url = await self.get_remote_url(permissions=permissions, ttl=ttl)
url = await self.get_remote_url(permissions=permissions, ttl=ttl, ops=ops)
return url.replace(".git", "+ephemeral.git")

async def get_file_stream(
Expand Down
12 changes: 12 additions & 0 deletions packages/code-storage-python/pierre_storage/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ class GitStorageOptions(TypedDict, total=False):
default_ttl: Optional[int]


# Op is a policy operation included in the JWT.
Op = str

OP_NO_FORCE_PUSH: Op = "no-force-push"

# Ops is a list of policy operations.
Ops = List[Op]


class PublicGitHubBaseRepoAuth(TypedDict):
"""Authentication mode for GitHub base repositories."""

Expand Down Expand Up @@ -508,6 +517,7 @@ async def get_remote_url(
*,
permissions: Optional[list[str]] = None,
ttl: Optional[int] = None,
ops: Optional[list[str]] = None,
) -> str:
"""Get the remote URL for the repository."""
...
Expand All @@ -517,6 +527,7 @@ async def get_import_remote_url(
*,
permissions: Optional[list[str]] = None,
ttl: Optional[int] = None,
ops: Optional[list[str]] = None,
) -> str:
"""Get the import remote URL for the repository."""
...
Expand All @@ -526,6 +537,7 @@ async def get_ephemeral_remote_url(
*,
permissions: Optional[list[str]] = None,
ttl: Optional[int] = None,
ops: Optional[list[str]] = None,
) -> str:
"""Get the ephemeral remote URL for the repository."""
...
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pierre-storage"
version = "1.5.0"
version = "1.5.1"
description = "Pierre Git Storage SDK for Python"
readme = "README.md"
license = "MIT"
Expand Down
35 changes: 35 additions & 0 deletions packages/code-storage-python/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,41 @@ def test_generate_jwt_with_all_parameters(self, test_key: str) -> None:
assert payload["scopes"] == ["git:write", "git:read", "repo:write"]
assert payload["exp"] - payload["iat"] == 7200

def test_generate_jwt_with_ops(self, test_key: str) -> None:
"""Test JWT generation with policy operations."""
token = generate_jwt(
key_pem=test_key,
issuer="test-customer",
repo_id="test-repo",
ops=["no-force-push"],
)

payload = jwt.decode(token, options={"verify_signature": False})
assert payload["ops"] == ["no-force-push"]

def test_generate_jwt_without_ops(self, test_key: str) -> None:
"""Test JWT generation omits ops when not provided."""
token = generate_jwt(
key_pem=test_key,
issuer="test-customer",
repo_id="test-repo",
)

payload = jwt.decode(token, options={"verify_signature": False})
assert "ops" not in payload

def test_generate_jwt_with_empty_ops(self, test_key: str) -> None:
"""Test JWT generation omits ops when empty list."""
token = generate_jwt(
key_pem=test_key,
issuer="test-customer",
repo_id="test-repo",
ops=[],
)

payload = jwt.decode(token, options={"verify_signature": False})
assert "ops" not in payload

def test_generate_jwt_default_ttl(self, test_key: str) -> None:
"""Test JWT generation uses 1 year default TTL."""
token = generate_jwt(
Expand Down
2 changes: 1 addition & 1 deletion packages/code-storage-python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/code-storage-typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pierre/storage",
"version": "1.4.0",
"version": "1.4.1",
"description": "Pierre Git Storage SDK",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions packages/code-storage-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,7 @@ export class GitStorage {
scopes: permissions,
iat: now,
exp: now + ttl,
...(options?.ops && options.ops.length > 0 ? { ops: options.ops } : {}),
};

// Sign the JWT with the key as the secret
Expand Down
9 changes: 9 additions & 0 deletions packages/code-storage-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ export interface GitStorageOptions extends OverrideableGitStorageOptions {

export type ValidAPIVersion = 1;

/** A policy operation included in the JWT. */
export type Op = string;

export const OP_NO_FORCE_PUSH: Op = 'no-force-push';

/** A list of policy operations. */
export type Ops = Op[];

export interface GetRemoteURLOptions {
permissions?: ('git:write' | 'git:read' | 'repo:write' | 'org:read')[];
ttl?: number;
ops?: Ops;
}

export interface Repo {
Expand Down
Loading
Loading